Skip to main content

aion_server/worker/
liminal_transport.rs

1//! Cross-node outbox dispatch over the liminal bus (LSUB push transport).
2//!
3//! # What this is (production push path)
4//!
5//! This module wires the durable outbox's fan-out dispatch over liminal to a
6//! REAL remote aion worker and returns the worker's result through the existing
7//! [`OutboxDeliveryCallback`](super::bridge::OutboxDeliveryCallback), behind the
8//! `liminal-transport` Cargo feature and the `outbox.transport = liminal`
9//! runtime flag. The aion-server HOSTS the liminal listener: a remote worker
10//! connects IN and self-describes in-band, the server registers it in the SAME
11//! connected-worker registry a gRPC worker joins, and a claimed row is PUSHED out
12//! on the worker's existing connection (the LSUB-0 server-push primitive).
13//!
14//! # Routing (NSTQ-5 / NODE-5)
15//!
16//! A worker is selected by the row's `(namespace, task_queue, activity_type,
17//! node)` pool key through the EXISTING registry `select_worker` — the same
18//! selection the gRPC path uses, so routing semantics are shared. `activity_type`
19//! is NOT a routing dimension at the wire: it rides inside the [`DispatchRequest`]
20//! payload and is matched by the worker after delivery, exactly as the gRPC
21//! registry pushes `activity_type` in the task body while selecting the worker by
22//! pool key. See `docs/NAMESPACE-TASKQUEUE-SPLIT-DESIGN.md` §4.2. The
23//! [`dispatch_channel_name`] derivation remains the single source of truth for the
24//! pool-channel string, pinned for any future channel-subscription subscriber so
25//! the two sides cannot drift.
26//!
27//! # The seams it implements
28//!
29//! - [`RegistryLiminalDispatch`] implements
30//!   [`OutboxRowDispatch`](super::outbox_dispatcher::OutboxRowDispatch): for each
31//!   claimed row it selects a worker from the connected-worker registry, pushes
32//!   the [`DispatchRequest`] to that worker's liminal connection via its
33//!   [`LiminalWorkerDelivery`], and re-enters the worker's [`DispatchResponse`]
34//!   through the SAME [`LiminalCompletionSource`] / [`OutboxDeliveryCallback`] the
35//!   gRPC completion path uses. A row that reaches no matching worker, or whose
36//!   worker is not liminal-delivered, returns an error so the outbox's unchanged
37//!   retry/backoff drives it — the same honest no-worker contract as the gRPC
38//!   path.
39//! - [`LiminalConnectionNotifier`] is the SERVER half of in-band registration:
40//!   when a worker connects with a [`WorkerRegistration`](WireWorkerRegistration)
41//!   the notifier inserts a [`WorkerDelivery::Liminal`] into the registry, and
42//!   drops it on disconnect.
43//! - [`LiminalCompletionSource`] maps a [`DispatchResponse`] onto the delivery
44//!   callback, threading `run_id` end-to-end so the existing continue-as-new run
45//!   gates apply unchanged.
46//!
47//! # The channel-subscription seam (documented, distinct from the push path)
48//!
49//! [`dispatch_channel_name`] derives the pool channel a `(namespace, task_queue)`
50//! pool addresses, optionally pinned to a `node` (NODE-2/NODE-5). The production
51//! path above does not publish to that channel — it pushes to a connected worker
52//! the server already owns — but the derivation is retained as the pinned contract
53//! any future channel-subscription transport MUST honour so the dispatcher and a
54//! subscriber cannot drift:
55//!
56//! - An UNPINNED worker pool addressed `(namespace, task_queue)` subscribes to
57//!   `dispatch_channel_name(namespace, task_queue, None)`.
58//! - A NODE-PINNED dispatch (the row carries `Some(node)`) maps to
59//!   `dispatch_channel_name(namespace, task_queue, Some(node))` — a DISTINCT
60//!   channel that a worker on that node must ALSO subscribe to in order to serve
61//!   pinned work; the unpinned channel alone never delivers a pinned dispatch.
62//!
63//! That is the single contract the seam must honour.
64
65use std::collections::HashMap;
66use std::sync::{Arc, Mutex, OnceLock};
67use std::time::Duration;
68
69use aion_core::{ActivityId, ContentType, Payload, RunId, WorkflowId};
70use aion_store::OutboxRow;
71use async_trait::async_trait;
72use liminal::protocol::WorkerRegistration as WireWorkerRegistration;
73use liminal_sdk::{SchemaMetadata, SchemaValidate};
74use liminal_server::ServerError as LiminalServerError;
75use liminal_server::server::connection::{
76    ConnectionNotifier, ConnectionSupervisor, PushReplyAwaiter,
77};
78use serde::{Deserialize, Serialize};
79
80use super::bridge::OutboxDeliveryCallback;
81use super::envelope::{CompletionFences, CompletionToken, idempotency_key};
82use super::outbox_dispatcher::{DeliveryGate, OutboxRowDispatch};
83use super::registry::{ConnectedWorkerRegistry, WorkerDelivery, WorkerHandle, WorkerRegistration};
84use crate::error::ServerError;
85
86/// Upper bound on how long a server-initiated intervention push waits for the
87/// worker's correlated control-plane acknowledgement. Activity dispatch replies
88/// use an unbounded held wait because an activity may legitimately run a while.
89const PUSH_REPLY_TIMEOUT: Duration = Duration::from_secs(30);
90
91/// Re-arm cadence for the engine-seam bridge's UNBOUNDED reply wait
92/// ([`receive_bridge_reply`]). Each elapsed poll is a benign re-arm, never a
93/// failure: the bridge dispatch contract imposes no activity timeout of its own
94/// (agent-style activities legitimately run for over an hour), exactly like the
95/// gRPC bridge's unbounded `recv`. Worker loss still terminates the wait
96/// promptly — the awaiter wakes with the typed Disconnected error the moment the
97/// connection closes.
98const BRIDGE_REPLY_POLL: Duration = Duration::from_secs(1);
99
100/// Wire request carrying one scheduled activity to a liminal worker.
101///
102/// Mirrors the dispatch half of the gRPC `ActivityTask`: the fields the worker
103/// needs to execute the activity and to correlate its result back to the exact
104/// execution (`workflow_id`, `ordinal`, `run_id`). `run_id` rides end-to-end so
105/// the existing continue-as-new run gates hold over the liminal wire (the design
106/// doc §3.3 requirement that `RunId` stays on the wire).
107#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
108pub struct DispatchRequest {
109    /// Activity type the worker must execute.
110    pub activity_type: String,
111    /// Workflow that scheduled this fan-out activity. Carried in its serde form
112    /// so no fragile id parsing happens on the wire.
113    pub workflow_id: WorkflowId,
114    /// Pinned ordinal of this activity within the workflow's fan-out range.
115    pub ordinal: u64,
116    /// Run that dispatched this ordinal, when known (continue-as-new safety).
117    pub run_id: Option<RunId>,
118    /// Opaque execution-generation proof echoed verbatim by the worker.
119    pub completion_token: String,
120    /// Stable external-effect key for this run and action site.
121    pub idempotency_key: String,
122    /// Opaque activity input bytes (JSON-tagged on the aion side).
123    pub input: Vec<u8>,
124    /// One-based delivery attempt, mirroring the gRPC `ActivityTask.attempt`.
125    /// The engine-seam bridge threads the real attempt so a retry executes with
126    /// attempt-aware handler semantics identical to the gRPC transport; the
127    /// outbox path stamps the row's stored zero-based attempt as one-based.
128    /// Serde-defaulted to `1` so a frame from a pre-attempt server (or an old
129    /// recorded frame) still decodes as a first delivery.
130    #[serde(default = "first_attempt")]
131    pub attempt: u32,
132    /// Engine-provided routing/metadata labels, mirroring the gRPC
133    /// `ActivityTask.labels`. Empty (the serde default) on the outbox path,
134    /// which has no label source.
135    #[serde(default)]
136    pub labels: std::collections::BTreeMap<String, String>,
137    /// The server's heartbeat window in milliseconds when this dispatch is
138    /// tracked by the server's per-task liveness tracker (the engine-seam
139    /// bridge path), or `0` when it is not (the outbox path, whose liveness
140    /// backstop is its own retry loop). A non-zero window tells the worker to
141    /// pump automatic liveness beats at a quarter-window cadence so the
142    /// server's heartbeat sweeper never expires a healthy long-running
143    /// activity — the exact liminal mirror of the gRPC worker's automatic
144    /// liveness pump.
145    #[serde(default)]
146    pub heartbeat_window_ms: u64,
147}
148
149/// Serde default for [`DispatchRequest::attempt`]: a frame that predates the
150/// attempt field is a first delivery.
151const fn first_attempt() -> u32 {
152    1
153}
154
155impl SchemaValidate for DispatchRequest {
156    fn schema_metadata() -> SchemaMetadata {
157        SchemaMetadata::new(
158            "aion.outbox.dispatch.request",
159            "1",
160            br#"{"type":"object"}"#.as_slice(),
161        )
162    }
163}
164
165/// Wire response carrying one worker result back to the outbox.
166///
167/// Mirrors the completion half of the gRPC `ActivityResult`: the correlation ids
168/// plus either a success result or a failure reason. `LiminalCompletionSource`
169/// maps this onto the existing [`OutboxDeliveryCallback`].
170#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
171pub struct DispatchResponse {
172    /// Workflow the completion belongs to.
173    pub workflow_id: WorkflowId,
174    /// Pinned ordinal the completion correlates against.
175    pub ordinal: u64,
176    /// Run that issued the dispatch, echoed back for the run gate.
177    pub run_id: Option<RunId>,
178    /// Opaque execution-generation proof echoed from the request.
179    pub completion_token: String,
180    /// Worker outcome: `Ok(result)` or `Err(reason)`.
181    pub outcome: Result<String, String>,
182}
183
184impl SchemaValidate for DispatchResponse {
185    fn schema_metadata() -> SchemaMetadata {
186        SchemaMetadata::new(
187            "aion.outbox.dispatch.response",
188            "1",
189            br#"{"type":"object"}"#.as_slice(),
190        )
191    }
192}
193
194/// Wire request carrying one neutral mid-run intervention command to a liminal
195/// worker (NOI-6, §6.2).
196///
197/// Rides the SAME liminal server-push channel as [`DispatchRequest`], distinguished
198/// on the wire by its unique required `intervention` field — a plain
199/// [`DispatchRequest`] has no such field, so the worker demuxes the two by which
200/// one deserializes. The whole envelope is neutral: it carries an
201/// [`InterventionCommand`], never a harness type. Field-for-field mirrored by the
202/// worker's `liminal::InterventionRequest`.
203#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
204pub struct InterventionRequest {
205    /// The neutral command to route to the worker owning the target attempt.
206    pub intervention: aion_core::InterventionCommand,
207}
208
209impl SchemaValidate for InterventionRequest {
210    fn schema_metadata() -> SchemaMetadata {
211        SchemaMetadata::new(
212            "aion.intervention.request",
213            "1",
214            br#"{"type":"object"}"#.as_slice(),
215        )
216    }
217}
218
219/// Wire response carrying the worker's neutral intervention ack back to the server
220/// (NOI-6). Field-for-field mirrored by the worker's `liminal::InterventionReply`.
221#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
222pub struct InterventionReply {
223    /// The neutral applied/gated/stale outcome the operator receives.
224    pub outcome: aion_core::InterventionOutcome,
225}
226
227impl SchemaValidate for InterventionReply {
228    fn schema_metadata() -> SchemaMetadata {
229        SchemaMetadata::new(
230            "aion.intervention.reply",
231            "1",
232            br#"{"type":"object"}"#.as_slice(),
233        )
234    }
235}
236
237/// Reserved liminal channel a worker publishes automatic liveness beats on.
238///
239/// The liminal wire has no gRPC-style heartbeat frame, so per-task liveness
240/// rides a reserved publish channel exactly as the observability transcript
241/// does: the worker's runtime pumps a [`WorkerLivenessBeat`] per in-flight
242/// tracked dispatch at a quarter-window cadence, and the server's
243/// [`LiminalConnectionNotifier`] consumes the channel and refreshes the shared
244/// [`HeartbeatTracker`] — so the #176 expiry sweeper is genuinely
245/// transport-agnostic: a healthy liminal worker running a long activity is
246/// never falsely expired, and a wedged one (which stops pumping) still is.
247/// Mirrored byte-for-byte by the worker crate's constant of the same name.
248pub const WORKER_LIVENESS_CHANNEL: &str = "aion.worker.liveness";
249
250/// Reserved liminal channel a worker announces its intervention capabilities on.
251///
252/// The in-band [`WireWorkerRegistration`] frame is a published liminal protocol
253/// type and cannot carry aion-level capability metadata, so a worker whose
254/// harness supports interventions publishes a [`WorkerCapabilitiesAnnouncement`]
255/// here immediately after registering (once per connection, so a redialed
256/// worker re-announces). The notifier consumes the channel and applies the
257/// announcement to the registered handle — the set the intervention router
258/// gates on and the ops console's live-attempts enumeration reports. Mirrored
259/// byte-for-byte by the worker crate's constant of the same name.
260pub const WORKER_CAPABILITIES_CHANNEL: &str = "aion.worker.capabilities";
261
262/// Wire announcement of a worker's advertised intervention capabilities.
263///
264/// Field-for-field mirror of the worker crate's `WorkerCapabilitiesAnnouncement`
265/// (the same cross-crate contract the dispatch/response pairs pin). The worker
266/// is identified by its CONNECTION (the publish's pid resolves the registered
267/// worker), never by wire-supplied identity, so an announcement can only ever
268/// apply to the worker that sent it.
269#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
270pub struct WorkerCapabilitiesAnnouncement {
271    /// The neutral intervention primitives the worker's harness supports.
272    pub capabilities: aion_core::InterventionCapabilities,
273}
274
275/// Wire liveness beat for one in-flight dispatch (the liminal mirror of the
276/// gRPC `Heartbeat` frame, liveness-only — progress payloads are not carried).
277///
278/// Field-for-field mirror of the worker crate's `WorkerLivenessBeat` (same
279/// serde field names + `aion-core` id types), the same cross-crate contract the
280/// dispatch/response pairs pin. The worker is identified by its CONNECTION (the
281/// publish's pid resolves the registered worker), never by wire-supplied
282/// identity, so a beat can only ever refresh tasks of the worker that sent it.
283#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
284pub struct WorkerLivenessBeat {
285    /// Workflow owning the in-flight activity being kept alive.
286    pub workflow_id: WorkflowId,
287    /// Pinned ordinal of the in-flight activity being kept alive.
288    pub ordinal: u64,
289}
290
291/// Builds the wire request for one claimed outbox row.
292///
293/// Kept free-standing (not a method) so both the dispatch path and tests build
294/// the request the same way.
295#[must_use]
296pub fn request_for_row(
297    row: &OutboxRow,
298    run_id: &RunId,
299    completion_token: &CompletionToken,
300) -> DispatchRequest {
301    let activity_id = ActivityId::from_sequence_position(row.ordinal);
302    DispatchRequest {
303        activity_type: row.activity_type.clone(),
304        workflow_id: row.workflow_id.clone(),
305        ordinal: row.ordinal,
306        run_id: Some(run_id.clone()),
307        completion_token: completion_token.as_str().to_owned(),
308        idempotency_key: idempotency_key(&row.workflow_id, run_id, &activity_id),
309        input: row.input.bytes().to_vec(),
310        // The stored zero-based attempt is stamped one-based on the wire (zero
311        // is malformed), exactly as the gRPC outbox arm's `to_scheduled` does.
312        attempt: row.attempt.saturating_add(1),
313        // Outbox rows carry no engine labels (the gRPC arm sends empty too).
314        labels: std::collections::BTreeMap::new(),
315        // Outbox dispatches are not tracked by the server's per-task liveness
316        // tracker — the outbox retry loop is their liveness backstop — so no
317        // window is assigned and the worker does not pump beats for them.
318        heartbeat_window_ms: 0,
319    }
320}
321
322/// The single reserved character that separates channel segments. Because
323/// `namespace`/`task_queue` are free-form, any occurrence of this byte INSIDE a
324/// segment must be escaped so it cannot be mistaken for the segment boundary.
325const SEGMENT_SEPARATOR: char = '.';
326
327/// The escape character used by [`encode_segment`]. It must itself be escaped so
328/// the encoding stays injective (otherwise `%2E` as a literal field value would
329/// collide with an encoded `.`).
330const SEGMENT_ESCAPE: char = '%';
331
332/// Percent-encodes the two reserved characters (`.` and `%`) inside one channel
333/// segment so distinct segment values can never collide across the join.
334///
335/// This is a minimal, deterministic, per-segment escape: a literal `.` becomes
336/// `%2E` and a literal `%` becomes `%25`; every other byte (including the empty
337/// string) passes through unchanged. Because both the separator AND the escape
338/// char are encoded, the mapping `value -> encoded` is injective: it is exactly
339/// reversible by replacing `%2E -> .` and `%25 -> %`, so two distinct values
340/// can never encode to the same string. Dot-free, percent-free inputs (the
341/// normal case, e.g. `"remote"`, `"gpu"`) are returned byte-for-byte unchanged,
342/// so existing channels are stable.
343fn encode_segment(segment: &str) -> String {
344    // Fast path: nothing reserved, return an owned copy unchanged.
345    if !segment.contains([SEGMENT_SEPARATOR, SEGMENT_ESCAPE]) {
346        return segment.to_owned();
347    }
348    let mut encoded = String::with_capacity(segment.len());
349    for ch in segment.chars() {
350        match ch {
351            // Encode the escape char FIRST so an already-present `%` cannot be
352            // confused with one we introduce for the separator.
353            SEGMENT_ESCAPE => encoded.push_str("%25"),
354            SEGMENT_SEPARATOR => encoded.push_str("%2E"),
355            other => encoded.push(other),
356        }
357    }
358    encoded
359}
360
361/// Derives the liminal dispatch channel for a worker pool addressed
362/// `(namespace, task_queue)`, optionally pinned to a specific `node`.
363///
364/// This is the **single, total source of truth** for the channel string: every
365/// site that needs the channel a `(namespace, task_queue[, node])` pool
366/// dispatches to — both this dispatcher and any future worker-pool subscription
367/// side — MUST call this function so the two sides cannot drift. The format is
368/// `"aion.dispatch.{namespace}.{task_queue}"` for an unpinned dispatch and
369/// `"aion.dispatch.{namespace}.{task_queue}.{node}"` when a `node` is pinned;
370/// each `{segment}` is independently passed through [`encode_segment`].
371///
372/// # The subscriber contract (the seam this function pins, NODE-5 / 13-x)
373///
374/// The subscriber side remains the documented seam (it does not exist yet; 13-0
375/// uses liminal's in-server echo responder). The contract both sides MUST honour:
376///
377/// - An **unpinned** worker pool addressed `(namespace, task_queue)` subscribes
378///   to `dispatch_channel_name(namespace, task_queue, None)` and receives every
379///   unpinned dispatch for that pool.
380/// - A **node-pinned** dispatch (the row carries `Some(node)`) goes to
381///   `dispatch_channel_name(namespace, task_queue, Some(node))`, a DISTINCT
382///   channel. A worker running on that node which is meant to serve pinned work
383///   for the pool MUST ALSO subscribe to that node-specific channel — the
384///   `None` channel alone will never deliver a node-pinned dispatch to it.
385///
386/// Because the `None` channel and any `Some(node)` channel are distinct strings,
387/// a node-pinned dispatch never reaches an unpinned-only subscriber and vice
388/// versa; node isolation is therefore enforced by the channel string itself.
389///
390/// # Injectivity (why the per-segment encode matters)
391///
392/// `namespace`, `task_queue` and `node` are all free-form (the design forbids
393/// preset categories), so a raw `format!` would be NON-injective: a `.` inside
394/// any field bleeds across the separator and pools the design declares disjoint
395/// collide onto one channel — e.g. `("a.b", "c", None)` and `("a", "b.c", None)`
396/// would both yield `aion.dispatch.a.b.c`, a cross-pool leak on the very
397/// isolation dimension this routing exists to keep separate. Encoding each
398/// segment independently (the separator `.` and the escape `%` are escaped within
399/// a segment) makes the map from `(namespace, task_queue, node)` to channel
400/// string injective: distinct triples always yield distinct channels, ACROSS
401/// segment counts too. The node segment is appended only for `Some(node)`, and
402/// because no encoded segment can contain a bare separator, a 2-segment channel
403/// (unpinned) can never be confused with a 3-segment channel (pinned) — e.g.
404/// `("a", "b", Some("c"))` and `("a", "b.c", None)` stay distinct, as do
405/// `("a.b", "c", None)` and `("a", "b", Some("c"))`.
406///
407/// `activity_type` is deliberately NOT part of the channel: it is *what to run*,
408/// matched by the worker after delivery (it rides inside [`DispatchRequest`]),
409/// not *which pool* — see `docs/NAMESPACE-TASKQUEUE-SPLIT-DESIGN.md` §4.2. The
410/// function is total (defined for every input) and stable (the same
411/// `(namespace, task_queue, node)` always yields the same channel).
412#[must_use]
413pub fn dispatch_channel_name(namespace: &str, task_queue: &str, node: Option<&str>) -> String {
414    let namespace = encode_segment(namespace);
415    let task_queue = encode_segment(task_queue);
416    match node {
417        Some(node) => {
418            let node = encode_segment(node);
419            format!("aion.dispatch.{namespace}.{task_queue}.{node}")
420        }
421        None => format!("aion.dispatch.{namespace}.{task_queue}"),
422    }
423}
424
425/// Derives the liminal dispatch channel for a claimed outbox row.
426///
427/// Thin wrapper over [`dispatch_channel_name`] reading the row's durable
428/// `(namespace, task_queue)` (NSTQ-2 columns) and its optional `node` (NODE-2):
429/// when `row.node` is `Some`, the row dispatches to the node-pinned sub-channel;
430/// when `None`, it derives the byte-identical unpinned channel. Kept
431/// free-standing so the dispatch path and tests derive the row's channel
432/// identically.
433#[must_use]
434pub fn channel_for_row(row: &OutboxRow) -> String {
435    dispatch_channel_name(&row.namespace, &row.task_queue, row.node.as_deref())
436}
437
438/// Wraps a reason in the existing worker-dispatch error so a non-deliverable
439/// dispatch drives the outbox's unchanged retry/backoff/dead-letter path. The
440/// row-derived `channel` is surfaced as the `activity_type` field for operator
441/// diagnostics (the field is a free-form context string on this transport's
442/// error).
443fn dispatch_error(channel: &str, reason: String) -> ServerError {
444    ServerError::WorkerDispatch {
445        namespace: "liminal".to_owned(),
446        activity_type: channel.to_owned(),
447        reason,
448    }
449}
450
451/// Receives one worker result over liminal and re-enters it into aion.
452///
453/// Holds the installed [`OutboxDeliveryCallback`] (the same prod
454/// `ServerOutboxDeliveryCallback` the gRPC completion path uses) and maps a
455/// [`DispatchResponse`] onto it, threading `run_id` so the continue-as-new run
456/// gates apply unchanged.
457pub struct LiminalCompletionSource {
458    callback: Arc<dyn OutboxDeliveryCallback>,
459    completion_fences: CompletionFences,
460}
461
462impl std::fmt::Debug for LiminalCompletionSource {
463    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464        f.debug_struct("LiminalCompletionSource")
465            .finish_non_exhaustive()
466    }
467}
468
469impl LiminalCompletionSource {
470    /// Build a completion source over the shared outbox delivery callback.
471    #[must_use]
472    pub fn new(callback: Arc<dyn OutboxDeliveryCallback>) -> Self {
473        Self {
474            callback,
475            completion_fences: CompletionFences::default(),
476        }
477    }
478
479    /// Share the authoritative generation registry used by dispatch.
480    #[must_use]
481    pub fn with_completion_fences(mut self, completion_fences: CompletionFences) -> Self {
482        self.completion_fences = completion_fences;
483        self
484    }
485
486    /// Re-enter one worker result into aion through the delivery callback.
487    ///
488    /// Returns the callback's `bool`: `true` when delivered to a live run,
489    /// `false` when no run is live (the expected stale-completion drop that
490    /// recovery re-arms). A success outcome routes to `deliver_completion`; a
491    /// failure outcome to `deliver_failure`.
492    ///
493    /// # Errors
494    ///
495    /// Returns [`ServerError`] when the response carries an unparseable id or
496    /// the engine rejects the delivery.
497    pub fn deliver(&self, response: &DispatchResponse) -> Result<bool, ServerError> {
498        let run_id = response.run_id.as_ref().ok_or_else(|| {
499            dispatch_error(
500                "liminal completion",
501                "activity result run id is missing; refusing run-gate bypass".to_owned(),
502            )
503        })?;
504        let activity_id = ActivityId::from_sequence_position(response.ordinal);
505        let completion_token = CompletionToken::from_wire(
506            &response.workflow_id,
507            &activity_id,
508            response.completion_token.clone(),
509        )?;
510        match self
511            .completion_fences
512            .accept(&response.workflow_id, &activity_id, &completion_token)
513        {
514            Ok(accepted) => {
515                // The consumed generation is bound and deliberately NOT
516                // restored: unlike the bridge, this path has no settlement that
517                // can fail after acceptance — the callback below IS the
518                // settlement and its failure is returned straight to the
519                // caller — so there is no `restore_if_absent` site on this
520                // transport for the slip to feed.
521                tracing::debug!(
522                    workflow_id = %response.workflow_id,
523                    %activity_id,
524                    attempt = accepted.attempt(),
525                    "liminal completion consumed the execution generation"
526                );
527            }
528            Err(error) => {
529                tracing::warn!(
530                    workflow_id = %response.workflow_id,
531                    %activity_id,
532                    %error,
533                    "liminal completion fence rejected a late or stale reply"
534                );
535                return Err(error);
536            }
537        }
538        match &response.outcome {
539            Ok(result) => self.callback.deliver_completion(
540                &response.workflow_id,
541                &activity_id,
542                Some(run_id),
543                result.clone(),
544            ),
545            Err(reason) => self.callback.deliver_failure(
546                &response.workflow_id,
547                &activity_id,
548                Some(run_id),
549                reason.clone(),
550            ),
551        }
552    }
553}
554
555/// Rebuilds the activity input payload from the wire request.
556///
557/// The aion side tags activity input as JSON; the wire carries the raw bytes, so
558/// a worker (or the test responder standing in for one) reconstructs the typed
559/// [`Payload`] with the JSON content type.
560#[must_use]
561pub fn payload_from_request(request: &DispatchRequest) -> Payload {
562    Payload::new(ContentType::Json, request.input.clone())
563}
564
565/// Delivery handle for a liminal-connected worker held in the worker registry.
566///
567/// A worker that connects over liminal is a first-class registry member selected
568/// the SAME way as a gRPC worker (`select_worker` on `(namespace, task_queue,
569/// node)`); this is the delivery leg the registry holds for it. It pairs the
570/// [`ConnectionSupervisor`] that owns the worker's connection with that
571/// connection's beamr `pid`, so [`Self::dispatch_held`] can push a
572/// [`DispatchRequest`] out on the worker's existing socket (the LSUB-0
573/// server-push primitive) and block for the correlated [`DispatchResponse`].
574#[derive(Clone)]
575pub struct LiminalWorkerDelivery {
576    supervisor: ConnectionSupervisor,
577    pid: u64,
578}
579
580impl std::fmt::Debug for LiminalWorkerDelivery {
581    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
582        f.debug_struct("LiminalWorkerDelivery")
583            .field("pid", &self.pid)
584            .finish_non_exhaustive()
585    }
586}
587
588impl LiminalWorkerDelivery {
589    /// Build a delivery handle for the worker reachable on connection `pid`
590    /// through `supervisor`.
591    #[must_use]
592    pub const fn new(supervisor: ConnectionSupervisor, pid: u64) -> Self {
593        Self { supervisor, pid }
594    }
595
596    /// The connection pid this worker is addressed on.
597    #[must_use]
598    pub const fn pid(&self) -> u64 {
599        self.pid
600    }
601
602    /// Push one outbox dispatch and wait without an activity-duration bound,
603    /// abandoning only when `keep_waiting` becomes false at a poll boundary.
604    ///
605    /// # Errors
606    ///
607    /// Returns the same typed push, receive, and decode failures as the shared
608    /// bridge wait.
609    pub fn dispatch_held(
610        &self,
611        request: &DispatchRequest,
612        keep_waiting: impl Fn() -> bool,
613    ) -> Result<Option<DispatchResponse>, ServerError> {
614        let awaiter = self.push_dispatch(request)?;
615        receive_bridge_reply(&awaiter, keep_waiting)
616    }
617
618    /// Serialize and push one dispatch out on the worker's connection, returning
619    /// the awaiter for its correlated reply — the shared push half of both
620    /// unbounded waits: [`Self::dispatch_held`] on the outbox path and the
621    /// engine-seam bridge dispatcher both use [`receive_bridge_reply`], matching
622    /// the gRPC bridge contract. One frame format, one push primitive, one held
623    /// wait policy.
624    ///
625    /// DELIBERATELY NO REPLY DEADLINE, unlike the liveness and intervention
626    /// pushes. A dispatch's reply IS the activity's completion, so its lifetime
627    /// is the activity's — `remote_gates.awl` declares legs at `timeout 45m`.
628    /// [`receive_bridge_reply`] re-arms on every poll timeout and waits
629    /// unbounded by contract, so this push NEVER abandons its slot and cannot
630    /// leak one: the slot is held for exactly as long as work is outstanding
631    /// and resolves when the reply lands. Attaching a deadline here would kill
632    /// long activities, and would be a regression, not a hardening. The leak
633    /// fixed on the other two sites came from ABANDONING a no-deadline slot,
634    /// which this site never does.
635    ///
636    /// # Errors
637    ///
638    /// Returns [`ServerError::WorkerBusy`] when liminal's typed connection cap
639    /// refuses admission, [`ServerError::WorkerConnectionLost`] for every other
640    /// push-enqueue failure, and [`ServerError::WorkerDispatch`] when the request
641    /// cannot be serialized.
642    pub(crate) fn push_dispatch(
643        &self,
644        request: &DispatchRequest,
645    ) -> Result<PushReplyAwaiter, ServerError> {
646        let payload = serde_json::to_vec(request).map_err(|error| {
647            dispatch_error("liminal-push", format!("request serialize failed: {error}"))
648        })?;
649        self.supervisor
650            .push_to_connection(self.pid, payload)
651            .map_err(|error| classify_push_error(&error))
652    }
653
654    /// Push already-encoded `payload` out on the worker's connection with an
655    /// explicit reply `deadline`, returning the awaiter for its correlated
656    /// reply.
657    ///
658    /// The raw push primitive behind the typed pushes above, used by the
659    /// connection liveness probe
660    /// ([`LivenessProbe`](super::liminal_liveness::LivenessProbe)) so the
661    /// dead-man switch rides the SAME server-push leg a dispatch does — a ping
662    /// that reaches the worker proves the exact path a dispatch would take, not
663    /// a parallel one that could be healthy while the real one is not.
664    ///
665    /// The deadline is LOAD-BEARING and is why this is not
666    /// `push_to_connection`. Riding the dispatch leg means sharing its bounded
667    /// §5 `max_pending_pushes_per_connection` admission, and a no-deadline slot
668    /// is reclaimed only by a consumed reply or a connection close — never by
669    /// the caller giving up. A probe that abandons an unanswered ping every
670    /// cadence therefore LEAKS one slot per round until the cap is exhausted,
671    /// at which point no dispatch, intervention or ping can be pushed on that
672    /// connection again. Measured live on run `dfd2117c`: 32 abandoned pings,
673    /// then permanent refusal, ≈240 s from first blocked ping. The probe built
674    /// to prove the dispatch path works is what destroyed it. With a deadline,
675    /// expiry removes the slot and RELEASES its cap admission.
676    ///
677    /// # Errors
678    ///
679    /// Returns [`ServerError::WorkerBusy`] when liminal's typed connection cap
680    /// refuses admission, and [`ServerError::WorkerConnectionLost`] for every
681    /// other push-enqueue failure.
682    pub fn push_payload_with_deadline(
683        &self,
684        payload: Vec<u8>,
685        deadline: Duration,
686    ) -> Result<PushReplyAwaiter, ServerError> {
687        self.supervisor
688            .push_to_connection_with_deadline(self.pid, payload, deadline)
689            .map_err(|error| classify_push_error(&error))
690    }
691
692    /// Push one neutral intervention command out on the worker's connection and
693    /// block for its correlated ack reply (NOI-6, §6.2).
694    ///
695    /// Uses the same connection as activity dispatch but carries an
696    /// [`InterventionRequest`] and decodes an [`InterventionReply`], so it rides the SAME server-push
697    /// channel as an activity dispatch. The push is a blocking, thread-based liminal
698    /// call; the async router runs it off the runtime.
699    ///
700    /// # Errors
701    ///
702    /// Returns [`ServerError::WorkerConnectionLost`] when the worker connection was
703    /// gone at push time or closed before replying (so the router surfaces the
704    /// too-late no-op); returns [`ServerError::WorkerDispatch`] when the request
705    /// cannot be serialized, the reply times out, or the reply cannot be decoded.
706    pub fn push_intervention(
707        &self,
708        request: &InterventionRequest,
709    ) -> Result<InterventionReply, ServerError> {
710        let payload = serde_json::to_vec(request).map_err(|error| {
711            dispatch_error(
712                "liminal-push",
713                format!("intervention serialize failed: {error}"),
714            )
715        })?;
716        // Deadline-bounded, and it must be: this wait ABANDONS on timeout (the
717        // `receive` error below is terminal, not a re-arm), so a no-deadline
718        // slot would be held until the connection closed and would count
719        // against the connection's §5 pending-push cap forever. The deadline
720        // matches the wait, so an intervention that goes unanswered releases
721        // its admission instead of leaking it. Contrast `push_dispatch`, whose
722        // wait re-arms and is unbounded by contract, and which therefore
723        // correctly takes no deadline.
724        let awaiter = self
725            .supervisor
726            .push_to_connection_with_deadline(self.pid, payload, PUSH_REPLY_TIMEOUT)
727            .map_err(|error| {
728                ServerError::worker_connection_lost(
729                    "liminal-push",
730                    format!("push intervention to worker failed: {error}"),
731                )
732            })?;
733        let reply = awaiter.receive(PUSH_REPLY_TIMEOUT).map_err(|error| {
734            if is_connection_closed_reply_error(&error) {
735                ServerError::worker_connection_lost(
736                    "liminal-push",
737                    format!("worker connection closed before intervention ack: {error}"),
738                )
739            } else {
740                dispatch_error("liminal-push", format!("intervention ack failed: {error}"))
741            }
742        })?;
743        serde_json::from_slice(&reply).map_err(|error| {
744            dispatch_error(
745                "liminal-push",
746                format!("intervention ack decode failed: {error}"),
747            )
748        })
749    }
750}
751
752/// Decodes one correlated reply payload as a [`DispatchResponse`].
753///
754/// Shared by the outbox wait ([`LiminalWorkerDelivery::dispatch_held`]) and the
755/// bridge wait ([`receive_bridge_reply`]) so the two paths can never diverge on
756/// the wire's reply shape.
757fn decode_dispatch_response(reply: &[u8]) -> Result<DispatchResponse, ServerError> {
758    serde_json::from_slice(reply).map_err(|error| {
759        dispatch_error(
760            "liminal-push",
761            format!("worker reply decode failed: {error}"),
762        )
763    })
764}
765
766/// Blocks for the correlated reply to an engine-seam BRIDGE dispatch push, with
767/// the bridge's UNBOUNDED wait contract: the engine imposes no activity timeout
768/// of its own, so an elapsed [`BRIDGE_REPLY_POLL`] merely re-arms the wait — the
769/// exact liminal mirror of the gRPC bridge's unbounded `recv`, which is released
770/// only by a completion or by stream teardown. The wait terminates on exactly:
771///
772/// - **the reply** — decoded as the worker's [`DispatchResponse`] and returned
773///   as `Ok(Some(response))`. A reply already buffered when a poll fires always
774///   wins over abandonment: the awaiter is drained before `keep_waiting` runs;
775/// - **abandonment** — `keep_waiting()` returned `false` at a poll boundary
776///   (the bridge passes "is this dispatch still tracked in-flight?"): the
777///   dispatch was resolved by another path (expiry sweep, shutdown drain, or a
778///   cleanup), so the wait returns `Ok(None)` and the router thread exits
779///   instead of parking on the connection indefinitely;
780/// - **worker loss** — the connection closed before replying: the awaiter wakes
781///   PROMPTLY with liminal's typed Disconnected error, surfaced as
782///   [`ServerError::WorkerConnectionLost`] so the bridge reports the same
783///   TRANSPORT-loss class the gRPC teardown sweep does;
784/// - **an unrecognized receive fault or a decode failure** — surfaced as
785///   [`ServerError::WorkerDispatch`].
786///
787/// Runs on a dedicated bridge reply thread, never on an async runtime worker.
788///
789/// # Errors
790///
791/// Returns [`ServerError::WorkerConnectionLost`] on worker loss and
792/// [`ServerError::WorkerDispatch`] on a receive fault or reply decode failure.
793pub(crate) fn receive_bridge_reply(
794    awaiter: &PushReplyAwaiter,
795    keep_waiting: impl Fn() -> bool,
796) -> Result<Option<DispatchResponse>, ServerError> {
797    loop {
798        match awaiter.receive(BRIDGE_REPLY_POLL) {
799            Ok(reply) => return decode_dispatch_response(&reply).map(Some),
800            // A bare poll timeout re-arms the wait while the dispatch is still
801            // outstanding (the wait is unbounded by contract; see the bridge
802            // module docs), and ends it cleanly once the dispatch was resolved
803            // elsewhere — the poll cadence bounds the router thread's lifetime
804            // to one poll past that resolution.
805            Err(LiminalServerError::PushReplyTimeout { .. }) => {
806                if !keep_waiting() {
807                    return Ok(None);
808                }
809            }
810            Err(error) if is_connection_closed_reply_error(&error) => {
811                return Err(ServerError::worker_connection_lost(
812                    "liminal-push",
813                    format!("worker connection closed before reply: {error}"),
814                ));
815            }
816            Err(error) => {
817                return Err(dispatch_error(
818                    "liminal-push",
819                    format!("worker reply failed: {error}"),
820                ));
821            }
822        }
823    }
824}
825
826/// Returns true when a liminal push-reply error is the *Disconnected* case (the
827/// worker's connection closed before it replied), as opposed to a genuine reply
828/// timeout (the worker is alive but slow).
829///
830/// Liminal returns these as distinct TYPED variants —
831/// `ServerError::PushReplyDisconnected` vs `PushReplyTimeout` (see `liminal-server`
832/// `supervisor.rs` `PushReplyAwaiter::receive`) — so this is a type match, not a
833/// message-text match: a worker that DIED (connection-lost, fast failover) is told
834/// apart from one that is merely SLOW (genuine timeout, normal backoff) by variant.
835fn is_connection_closed_reply_error(error: &LiminalServerError) -> bool {
836    matches!(error, LiminalServerError::PushReplyDisconnected { .. })
837}
838
839/// Classify a typed liminal push refusal without relying on error text.
840fn classify_push_error(error: &LiminalServerError) -> ServerError {
841    if is_connection_cap_error(error) {
842        ServerError::worker_busy(
843            "liminal-push",
844            format!("worker connection push cap reached: {error}"),
845        )
846    } else {
847        ServerError::worker_connection_lost(
848            "liminal-push",
849            format!("push to worker failed: {error}"),
850        )
851    }
852}
853
854/// Liminal's typed admission refusal when a connection has too many held pushes.
855fn is_connection_cap_error(error: &LiminalServerError) -> bool {
856    matches!(error, LiminalServerError::ConnectionCapReached { .. })
857}
858
859/// Cross-node [`OutboxRowDispatch`] that selects a liminal worker from the
860/// connected-worker registry and pushes the row to it.
861///
862/// This is the LSUB-1 server-side composition: for each claimed row it selects a
863/// worker by the row's `(namespace, task_queue, activity_type, node)` via the
864/// EXISTING registry `select_worker` (the same selection the gRPC path uses, so
865/// routing semantics are shared), pushes the [`DispatchRequest`] to that worker's
866/// liminal connection via its [`LiminalWorkerDelivery`], and re-enters the
867/// worker's [`DispatchResponse`] through the SAME [`LiminalCompletionSource`] /
868/// [`OutboxDeliveryCallback`] the existing completion path uses. A row that
869/// reaches no matching worker, or whose worker is not liminal-delivered, returns
870/// an error so the outbox's unchanged retry/backoff drives it — the same honest
871/// no-worker contract as the gRPC path.
872pub struct RegistryLiminalDispatch {
873    registry: ConnectedWorkerRegistry,
874    completion: LiminalCompletionSource,
875    delivery_gate: DeliveryGate,
876    /// Optional short-TTL per-namespace placement cache (Control-Plane Phase 2,
877    /// P2-P3), the SAME cache the gRPC
878    /// [`WorkerOutboxDispatch`](crate::worker::WorkerOutboxDispatch) is given. When
879    /// present, an UNPINNED row (`row.node == None`) whose namespace placement is
880    /// `Prefer{L}` selects an L-labelled worker and spills to any live worker when
881    /// none is up, via the SHARED
882    /// [`preferred_node_order`](crate::worker::preferred_node_order). When absent
883    /// (the default, every pre-Phase-2 construction and test) selection is
884    /// byte-identical to before: one `select_worker` off the row's own node.
885    /// Placement is NEVER stamped back onto the row — it is consulted only here,
886    /// in this non-replayed dispatcher, for worker selection.
887    placement_cache: Option<crate::worker::PlacementCache>,
888    /// Optional NOI-6 `attempt -> owning-worker` back-index. When installed via
889    /// [`Self::with_attempt_owners`], each dispatched agent attempt binds its
890    /// `(workflow, activity, attempt)` to the selected worker here BEFORE the push and
891    /// releases it after the reply, so the server's intervention router resolves the
892    /// CURRENT owner of a live attempt. `None` (the default, and every non-agent
893    /// deployment) skips the binding — intervention is simply never offered.
894    attempt_owners: Option<super::intervention::AttemptOwnerIndex>,
895}
896
897impl std::fmt::Debug for RegistryLiminalDispatch {
898    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
899        f.debug_struct("RegistryLiminalDispatch")
900            .field("placement_cache", &self.placement_cache.is_some())
901            .finish_non_exhaustive()
902    }
903}
904
905/// One selection attempt: the worker it found, and the node filters it WALKED.
906///
907/// The tiers travel WITH the outcome because a miss can only be explained over
908/// the filters selection actually applied. Recomputing them at the refusal site
909/// is how the two answers drift: the recomputation has no access to the
910/// namespace placement the selection resolved, so it silently substitutes the
911/// row's own node and describes a different fleet from the one selection saw.
912struct SelectionAttempt {
913    /// The selected worker, or `None` when no tier held an eligible one.
914    worker: Option<WorkerHandle>,
915    /// The ordered node filters walked, in the order they were tried. For a
916    /// `Pinned{L}` namespace these are the required labels and nothing else —
917    /// there is deliberately no `None` any-node tier to fall back on.
918    tiers: Vec<Option<String>>,
919}
920
921impl RegistryLiminalDispatch {
922    /// Build a registry-backed liminal dispatch that re-enters worker results
923    /// through `callback` (the shared `ServerOutboxDeliveryCallback`).
924    #[must_use]
925    pub fn new(
926        registry: ConnectedWorkerRegistry,
927        callback: Arc<dyn OutboxDeliveryCallback>,
928        delivery_gate: DeliveryGate,
929    ) -> Self {
930        Self {
931            registry,
932            completion: LiminalCompletionSource::new(callback),
933            delivery_gate,
934            placement_cache: None,
935            attempt_owners: None,
936        }
937    }
938
939    /// Share completion-generation authority with result ingestion.
940    #[must_use]
941    pub fn with_completion_fences(mut self, completion_fences: CompletionFences) -> Self {
942        self.completion = self.completion.with_completion_fences(completion_fences);
943        self
944    }
945
946    /// Install the NOI-6 attempt-owner back-index so each dispatched attempt binds
947    /// its owning worker for the intervention router to resolve (NOI-6).
948    ///
949    /// The SAME index the server's [`InterventionRouter`](super::intervention::InterventionRouter)
950    /// resolves through (from `ServerState::attempt_owners`), so a pushed command
951    /// reaches the worker this dispatcher sent the attempt to. Pure builder addition:
952    /// without it, no ownership is recorded and the router finds no owner (the
953    /// too-late no-op), exactly as before.
954    #[must_use]
955    pub fn with_attempt_owners(
956        mut self,
957        attempt_owners: super::intervention::AttemptOwnerIndex,
958    ) -> Self {
959        self.attempt_owners = Some(attempt_owners);
960        self
961    }
962
963    /// Attach the per-namespace placement cache so an unpinned row consults its
964    /// namespace's `Prefer` directive at selection time (Control-Plane Phase 2,
965    /// P2-P3) — the liminal mirror of
966    /// [`WorkerOutboxDispatch::with_placement_cache`](crate::worker::WorkerOutboxDispatch::with_placement_cache).
967    /// Pure builder addition: without it, selection is byte-identical to the
968    /// pre-Phase-2 behaviour.
969    #[must_use]
970    pub fn with_placement_cache(mut self, cache: crate::worker::PlacementCache) -> Self {
971        self.placement_cache = Some(cache);
972        self
973    }
974
975    /// Select the liminal worker for `row`, applying the SHARED placement decision
976    /// for an UNPINNED row when a placement cache is attached — the exact gRPC
977    /// semantics ([`worker_selection_for`](crate::worker::worker_selection_for)):
978    /// `Prefer{L}` spills to any live worker, `Pinned{L}` requires an L-labelled
979    /// worker and NEVER spills to a node=None any-worker.
980    ///
981    /// A per-activity authored pin (`row.node == Some(N)`) ALWAYS wins and is
982    /// selected off the row's own node, untouched by placement — exactly the gRPC
983    /// composition rule. Without a cache (or with a pinned row) this collapses to
984    /// the single `select_worker` off the row's own node — the pre-Phase-2
985    /// behaviour.
986    ///
987    /// For `Pinned{L}`, when no L-labelled worker is live this returns `Ok(None)` —
988    /// NOT a spill to a node=None worker — so the [`OutboxRowDispatch`] surfaces the
989    /// honest no-worker error and the outbox retries/stalls until an L-labelled
990    /// worker returns, mirroring the gRPC wait-for-worker path exactly (both
991    /// transports agree via [`WorkerSelection`](crate::worker::WorkerSelection)).
992    async fn select_liminal_worker(
993        &self,
994        row: &OutboxRow,
995    ) -> Result<SelectionAttempt, ServerError> {
996        // A pinned row or an absent cache: one selection off the row's own node.
997        let (Some(cache), None) = (&self.placement_cache, &row.node) else {
998            let tiers = vec![row.node.clone()];
999            let worker = self.registry.select_worker(
1000                &row.namespace,
1001                &row.task_queue,
1002                &row.activity_type,
1003                row.node.as_deref(),
1004            )?;
1005            return Ok(SelectionAttempt { worker, tiers });
1006        };
1007        // Unpinned + placement-aware: resolve the shared selection decision, so this
1008        // liminal path and the gRPC path can never diverge on Prefer-vs-Pinned. The
1009        // row's `node` is never mutated — selection is a pure dispatch-time input.
1010        let placement = cache.placement(&row.namespace).await;
1011        let tiers: Vec<Option<String>> = match crate::worker::worker_selection_for(&placement) {
1012            // Prefer/Unplaced: walk the prefer-then-spill tiers (the `None` spill is
1013            // always last), stopping at the first tier with a live worker.
1014            crate::worker::WorkerSelection::PreferTiers(tiers) => tiers,
1015            // Pinned{L}: try ONLY the required labels — no `None` spill. When none is
1016            // live, select nothing so the caller retries/stalls (never any-node).
1017            crate::worker::WorkerSelection::Required(required) => {
1018                required.into_iter().map(Some).collect()
1019            }
1020        };
1021        let worker = self.select_over_tiers(row, tiers.iter().map(Option::as_deref))?;
1022        Ok(SelectionAttempt { worker, tiers })
1023    }
1024
1025    /// Select the first live worker over an ordered sequence of node filters,
1026    /// returning `Ok(None)` when no filter matches a live worker. Shared by the
1027    /// `Prefer` (tiers end in a `None` spill) and `Pinned` (required labels only,
1028    /// no spill) selection arms so both walk the registry identically.
1029    fn select_over_tiers<'a>(
1030        &self,
1031        row: &OutboxRow,
1032        tiers: impl Iterator<Item = Option<&'a str>>,
1033    ) -> Result<Option<WorkerHandle>, ServerError> {
1034        for tier in tiers {
1035            let selected = self.registry.select_worker(
1036                &row.namespace,
1037                &row.task_queue,
1038                &row.activity_type,
1039                tier,
1040            )?;
1041            if selected.is_some() {
1042                return Ok(selected);
1043            }
1044        }
1045        Ok(None)
1046    }
1047
1048    /// Why this row found no worker — stated as the fact it IS (#197 R3).
1049    ///
1050    /// A selection miss has two causes an operator must act on differently, and
1051    /// the single catch-all this replaces conflated them into a misdiagnosis:
1052    ///
1053    /// - **nobody is there.** No worker registered for the row's address. The
1054    ///   remedy is to start one.
1055    /// - **workers are there and none is currently eligible.** They registered
1056    ///   and the liveness verdict excludes them — each is either still serving
1057    ///   its opening probation (ordinary, self-clearing within a couple of
1058    ///   probe cadences) or has had its eligibility withdrawn (an incident, and
1059    ///   the WARN naming it is already in the log). The remedy is to read that
1060    ///   verdict, and NEVER to start another worker — a second one would serve
1061    ///   the same probation and change nothing.
1062    ///
1063    /// The distinction became newly visible when the probe learned to reach
1064    /// gRPC-delivered workers: before, no gRPC worker was ever pinged, so its
1065    /// exclusion was permanent and invisible behind "nobody is registered".
1066    ///
1067    /// `tiers` MUST be the sequence [`Self::select_liminal_worker`] actually
1068    /// walked, which is why it travels here from the selection rather than
1069    /// being re-derived. A `Pinned{L}` namespace walks the required labels and
1070    /// never spills to a `None` any-node tier; counting over the row's own node
1071    /// instead would match every worker in the pool and blame the miss on an
1072    /// unlabelled worker that was never a candidate — telling the operator not
1073    /// to start the labelled worker that is the only remedy.
1074    ///
1075    /// A census read failure falls back to the bare miss, LOUDLY. Losing the
1076    /// REASON must never suppress the refusal itself, and inventing an
1077    /// exclusion count this call could not read would be worse than saying
1078    /// less — but a poisoned registry lock is a corruption signal that must not
1079    /// leave only a quieter refusal behind as its trace.
1080    fn selection_miss_reason(&self, row: &OutboxRow, tiers: &[Option<String>]) -> String {
1081        let excluded = match self.registry.ineligible_workers_over_tiers(
1082            &row.namespace,
1083            &row.task_queue,
1084            &row.activity_type,
1085            tiers,
1086        ) {
1087            Ok(excluded) => excluded,
1088            Err(error) => {
1089                tracing::warn!(
1090                    %error,
1091                    namespace = %row.namespace,
1092                    task_queue = %row.task_queue,
1093                    activity_type = %row.activity_type,
1094                    "could not read the dispatch-eligibility census for a selection miss; this \
1095                     refusal falls back to the bare no-worker message and may therefore describe \
1096                     a pool whose workers are merely EXCLUDED as though it were empty"
1097                );
1098                0
1099            }
1100        };
1101        if excluded == 0 {
1102            return "no liminal worker registered for the row's pool".to_owned();
1103        }
1104        format!(
1105            "{excluded} worker(s) are registered for the row's pool but NONE is currently \
1106             dispatch-eligible: each is either serving its opening liveness probation or has had \
1107             its eligibility withdrawn. This is not an empty pool — starting another worker will \
1108             not help. Read the liveness verdict for these workers (the probation/withdrawal lines \
1109             carry the worker ids and the reason)"
1110        )
1111    }
1112
1113    /// Withdraw the authorization THIS dispatch minted, and only that one: a
1114    /// sibling token held by a worker still executing the same attempt is left
1115    /// standing, because its result is real.
1116    fn revoke_completion(
1117        &self,
1118        row: &OutboxRow,
1119        activity_id: &ActivityId,
1120        completion_token: &CompletionToken,
1121    ) -> Result<(), ServerError> {
1122        self.completion
1123            .completion_fences
1124            .revoke(&row.workflow_id, activity_id, completion_token)
1125    }
1126}
1127
1128#[async_trait]
1129impl OutboxRowDispatch for RegistryLiminalDispatch {
1130    async fn dispatch(&self, row: &OutboxRow) -> Result<(), ServerError> {
1131        // Select the worker the SAME way the gRPC path does: by the row's
1132        // (namespace, task_queue, activity_type) pool key with the row's optional
1133        // node affinity, applying the SHARED `Prefer` two-tier spill for an
1134        // unpinned row when a placement cache is attached. No worker for the pool
1135        // => honest no-worker error => the outbox retries (never a false `done`).
1136        let attempt = self.select_liminal_worker(row).await?;
1137        let Some(worker) = attempt.worker else {
1138            return Err(dispatch_error(
1139                &channel_for_row(row),
1140                self.selection_miss_reason(row, &attempt.tiers),
1141            ));
1142        };
1143
1144        let delivery = match worker.delivery() {
1145            WorkerDelivery::Liminal(delivery) => delivery.clone(),
1146            WorkerDelivery::Grpc(_) => {
1147                return Err(dispatch_error(
1148                    &channel_for_row(row),
1149                    "selected worker is not delivered over liminal".to_owned(),
1150                ));
1151            }
1152        };
1153
1154        // The run is resolved BEFORE the owner binding, because the binding is
1155        // keyed on it: a run-blind key would let an intervention aimed at one
1156        // continue-as-new generation resolve another's worker. The refusal is
1157        // the pre-existing one — a row without a run cannot be dispatched at all
1158        // — simply moved ahead of the bind it now feeds.
1159        let activity_id = ActivityId::from_sequence_position(row.ordinal);
1160        let run_id = row.run_id.as_ref().ok_or_else(|| {
1161            dispatch_error(
1162                &channel_for_row(row),
1163                "activity run id is missing; refusing unfenced external effect".to_owned(),
1164            )
1165        })?;
1166
1167        // NOI-6: bind this attempt's owner BEFORE the push, so an intervention that
1168        // races the dispatch resolves the worker. The guard releases on every exit
1169        // path (reply, error, panic) so the index never keeps a finished attempt.
1170        // The key mirrors the worker's execute-path stamp exactly: activity_id from
1171        // the ordinal, run_id from the row, attempt = the wire's one-based delivery
1172        // attempt (the same `request_for_row` stamp the worker echoes into its
1173        // session key). See `LiminalActivityWorker::execute` / `run_agent_dispatch`.
1174        let _owner_guard = self.attempt_owners.as_ref().map(|owners| {
1175            AttemptOwnerGuard::bind(
1176                owners.clone(),
1177                super::intervention::AttemptKey::new(
1178                    row.workflow_id.clone(),
1179                    run_id.clone(),
1180                    activity_id.clone(),
1181                    row.attempt.saturating_add(1),
1182                ),
1183                worker.id(),
1184            )
1185        });
1186
1187        // Push the dispatch to the worker and block for its correlated reply. The
1188        // push is a blocking, thread-based liminal call; run it off the async
1189        // runtime so a long-running activity cannot starve a runtime worker.
1190        // Same run, same one-based attempt stamp the wire carries: a
1191        // redelivery of THIS attempt adds a sibling authorization rather than
1192        // displacing the token a worker is still holding.
1193        let completion_token = self.completion.completion_fences.issue(
1194            &row.workflow_id,
1195            run_id,
1196            &activity_id,
1197            row.attempt.saturating_add(1),
1198        )?;
1199        let request = request_for_row(row, run_id, &completion_token);
1200        let gate = self.delivery_gate.clone();
1201        let dispatch_key = row.dispatch_key.clone();
1202        let registry = self.registry.clone();
1203        let worker_id = worker.id();
1204        let dispatched = tokio::task::spawn_blocking(move || {
1205            delivery.dispatch_held(&request, || {
1206                if gate.is_draining() || !gate.holds(&dispatch_key) {
1207                    return false;
1208                }
1209                match registry.worker_by_id(worker_id) {
1210                    Ok(Some(_)) => true,
1211                    Ok(None) => false,
1212                    Err(error) => {
1213                        tracing::warn!(
1214                            %error,
1215                            %dispatch_key,
1216                            "delivery wait could not verify worker registration; abandoning"
1217                        );
1218                        false
1219                    }
1220                }
1221            })
1222        })
1223        .await
1224        .map_err(|error| {
1225            dispatch_error(
1226                &channel_for_row(row),
1227                format!("dispatch task join failed: {error}"),
1228            )
1229        });
1230        let response = match dispatched {
1231            Ok(Ok(Some(response))) => response,
1232            Ok(Ok(None)) => {
1233                tracing::warn!(
1234                    dispatch_key = %row.dispatch_key,
1235                    workflow_id = %row.workflow_id,
1236                    %activity_id,
1237                    "delivery wait abandoned; late reply will be discarded"
1238                );
1239                self.revoke_completion(row, &activity_id, &completion_token)?;
1240                return Err(dispatch_error(
1241                    &channel_for_row(row),
1242                    "delivery wait abandoned before worker reply".to_owned(),
1243                ));
1244            }
1245            Ok(Err(error)) | Err(error) => {
1246                self.revoke_completion(row, &activity_id, &completion_token)?;
1247                return Err(error);
1248            }
1249        };
1250
1251        // Re-enter the worker's result through the SAME completion path the gRPC
1252        // transport uses (terminal dedup in `record_fan_out_completion` applies
1253        // unchanged). The dispatch itself succeeded — the row's terminal state is
1254        // recorded by the completion callback, exactly as in the gRPC path.
1255        if let Err(error) = self.completion.deliver(&response) {
1256            self.revoke_completion(row, &activity_id, &completion_token)?;
1257            return Err(error);
1258        }
1259        Ok(())
1260    }
1261}
1262
1263/// RAII guard that releases an [`AttemptOwnerIndex`](super::intervention::AttemptOwnerIndex)
1264/// binding when the dispatch resolves — on the reply, an error, or a panic — so
1265/// the back-index tracks exactly the attempts currently in flight (NOI-6).
1266///
1267/// Shared by both liminal dispatch arms: the outbox row wait holds it across
1268/// its blocking `dispatch` call, and the engine-seam bridge hands it to the
1269/// dispatch's reply-router thread, which drops it on every exit path.
1270pub(crate) struct AttemptOwnerGuard {
1271    owners: super::intervention::AttemptOwnerIndex,
1272    key: super::intervention::AttemptKey,
1273}
1274
1275impl AttemptOwnerGuard {
1276    /// Bind `key` to `worker` in `owners` and return the guard that releases
1277    /// the binding on drop.
1278    pub(crate) fn bind(
1279        owners: super::intervention::AttemptOwnerIndex,
1280        key: super::intervention::AttemptKey,
1281        worker: super::registry::WorkerId,
1282    ) -> Self {
1283        owners.bind(key.clone(), worker);
1284        Self { owners, key }
1285    }
1286}
1287
1288impl Drop for AttemptOwnerGuard {
1289    fn drop(&mut self) {
1290        self.owners.release(&self.key);
1291    }
1292}
1293
1294/// Normalize a wire `node` (`Option<String>`) onto the registry's optional
1295/// locality affinity, applying the SAME none-convention the gRPC registration
1296/// path uses (`registry::optional_node`): an empty string carries no node, so it
1297/// collapses to `None`; any non-empty value is the worker's advertised node.
1298///
1299/// The wire already models `node` as `Option<String>`, but a worker that joins
1300/// `Some("")` (the empty-string node) must not register a distinct empty-node
1301/// affinity that no pinned dispatch could ever match — it is semantically
1302/// unpinned, exactly as the gRPC proto3 empty default is. Folding it to `None`
1303/// here keeps the two registration paths byte-for-byte equivalent.
1304fn normalize_wire_node(node: Option<&str>) -> Option<String> {
1305    node.filter(|value| !value.is_empty())
1306        .map(ToOwned::to_owned)
1307}
1308
1309/// Connection-keyed [`ConnectionNotifier`] that turns liminal's in-band worker
1310/// registration into a first-class [`ConnectedWorkerRegistry`] membership.
1311///
1312/// This is the SERVER half of LSUB-L2: when a worker connects with a
1313/// [`WireWorkerRegistration`] (the SDK's `connect_with_registration`), liminal's
1314/// connection process invokes [`on_worker_registered`](Self::on_worker_registered)
1315/// with the connection's beamr `pid` and the worker's declared
1316/// `(namespaces, task_queue, node, activity_types)`. The notifier builds a
1317/// [`WorkerDelivery::Liminal`] over the connection and inserts it into the
1318/// registry — the SAME registry entry, selected the SAME way, as a gRPC worker —
1319/// retiring the LSUB-1 out-of-band `active_connection_pids()` + hard-coded
1320/// registration hack.
1321///
1322/// # Lifetime of the registration guard
1323///
1324/// [`ConnectedWorkerRegistry::register_delivery`] returns a
1325/// [`WorkerRegistration`] guard whose drop deregisters the worker. The notifier
1326/// OWNS that guard keyed by `pid` (`Mutex<HashMap<u64, WorkerRegistration>>`), so
1327/// the registration lives exactly as long as the connection: it is inserted on
1328/// register and removed (dropped) on
1329/// [`on_worker_unregistered`](Self::on_worker_unregistered), which liminal fires
1330/// on connection close.
1331///
1332/// # Construction-order cycle (notifier <-> supervisor)
1333///
1334/// [`LiminalWorkerDelivery`] needs a [`ConnectionSupervisor`] handle to push to
1335/// the worker's connection, but the supervisor is itself constructed WITH this
1336/// notifier ([`ConnectionSupervisor::with_services_and_notifier`]) — a cycle. The
1337/// notifier therefore holds the supervisor behind a [`OnceLock`], populated
1338/// IMMEDIATELY after the supervisor is built via [`Self::bind_supervisor`]. The
1339/// `OnceLock` is never read before it is set in correct wiring (a worker can only
1340/// register after the listener — built after the supervisor and after
1341/// `bind_supervisor` — accepts its connection); if it somehow were, registration
1342/// is REJECTED with a typed error rather than panicking, so there is no
1343/// production `unwrap`/`expect` and no second always-`None` code path.
1344pub struct LiminalConnectionNotifier {
1345    registry: ConnectedWorkerRegistry,
1346    contract_catalog: Option<Arc<aion::Engine>>,
1347    supervisor: OnceLock<ConnectionSupervisor>,
1348    guards: Mutex<HashMap<u64, WorkerRegistration>>,
1349    /// The neutral intervention primitives a liminal-connected agent worker
1350    /// advertises (NOI-6, item 4). The liminal `WorkerRegistration` wire has a fixed
1351    /// shape that cannot carry this, so it is configured on the notifier at the
1352    /// composition root from the harness's advertised `AgentSession::capabilities()`
1353    /// and recorded on every registered worker's handle, where the intervention
1354    /// router gates on it. Default empty = observability-only (a plain activity
1355    /// worker), so the router offers no controls for it.
1356    intervention_capabilities: aion_core::InterventionCapabilities,
1357    /// The transcript sequencer a worker's observability publishes drain into
1358    /// (NOI-5b), plus the Tokio [`Handle`](tokio::runtime::Handle) to bridge the
1359    /// synchronous connection-process callback onto the async publish. `None` (the
1360    /// default, and every non-agent boot) makes the observability tap a no-op, so a
1361    /// worker publish to the reserved channel is simply ignored by the notifier.
1362    transcript: Option<TranscriptTap>,
1363    /// The SAME per-task liveness tracker the bridge dispatcher tracks into and
1364    /// the #176 expiry sweeper expires from. A worker's automatic
1365    /// [`WorkerLivenessBeat`] publishes on [`WORKER_LIVENESS_CHANNEL`] refresh
1366    /// their task stamps here, keeping the sweeper honest for liminal-delivered
1367    /// dispatches. `None` (a wiring that never bridges dispatches, e.g. isolated
1368    /// tests) consumes and drops the beats.
1369    heartbeat_tracker: Option<super::heartbeat::HeartbeatTracker>,
1370}
1371
1372/// The observability-drain leg of the notifier: a bounded, ORDERED queue into
1373/// the one drain task that publishes transcript events sequentially.
1374///
1375/// One consumer is load-bearing, not an implementation detail: a spawned task
1376/// per event (the pre-2026-07-23 shape) made every in-flight frame a
1377/// concurrent writer racing the same stream head, turning the sequencer's
1378/// optimistic-append loop into an O(N²) conflict stampede under load — and
1379/// destroyed `worker_seq` order on the durable transcript. Sequential
1380/// draining preserves arrival order and leaves the conflict-retry loop to
1381/// handle only genuine cross-process races (failover adoption).
1382#[derive(Clone)]
1383struct TranscriptTap {
1384    queue: tokio::sync::mpsc::Sender<aion_core::ActivityEvent>,
1385}
1386
1387/// Bound on the transcript drain queue: events a slow durable append cannot
1388/// keep up with are dropped (with a warning) rather than buffered without
1389/// limit. Live streaming is best-effort; the bound protects server memory.
1390const TRANSCRIPT_QUEUE_CAPACITY: usize = 4096;
1391
1392impl std::fmt::Debug for LiminalConnectionNotifier {
1393    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1394        f.debug_struct("LiminalConnectionNotifier")
1395            .field("supervisor_bound", &self.supervisor.get().is_some())
1396            .finish_non_exhaustive()
1397    }
1398}
1399
1400impl LiminalConnectionNotifier {
1401    /// Build a notifier that registers connecting workers into `registry`.
1402    ///
1403    /// The supervisor handle is bound separately via [`Self::bind_supervisor`]
1404    /// immediately after the supervisor is constructed, resolving the
1405    /// notifier <-> supervisor construction cycle (see the type docs).
1406    #[must_use]
1407    pub fn new(registry: ConnectedWorkerRegistry) -> Self {
1408        Self {
1409            registry,
1410            contract_catalog: None,
1411            supervisor: OnceLock::new(),
1412            guards: Mutex::new(HashMap::new()),
1413            intervention_capabilities: aion_core::InterventionCapabilities::none(),
1414            transcript: None,
1415            heartbeat_tracker: None,
1416        }
1417    }
1418
1419    /// Install the live package catalog used to refuse incompatible workers
1420    /// before liminal acknowledges or publishes their registration.
1421    #[must_use]
1422    pub fn with_contract_catalog(mut self, engine: Arc<aion::Engine>) -> Self {
1423        self.contract_catalog = Some(engine);
1424        self
1425    }
1426
1427    /// Install the shared per-task liveness tracker so a worker's automatic
1428    /// [`WorkerLivenessBeat`] publishes refresh the SAME in-flight entries the
1429    /// bridge dispatcher tracks and the #176 sweeper expires.
1430    ///
1431    /// MUST be wired on every boot that hosts the engine-seam bridge (the
1432    /// production composition does), or the sweeper would expire healthy
1433    /// liminal workers running activities longer than the heartbeat window.
1434    /// Without it (isolated tests that never bridge-dispatch) beats are
1435    /// consumed and dropped.
1436    #[must_use]
1437    pub fn with_heartbeat_tracker(mut self, tracker: super::heartbeat::HeartbeatTracker) -> Self {
1438        self.heartbeat_tracker = Some(tracker);
1439        self
1440    }
1441
1442    /// Install the transcript sequencer a worker's observability publishes drain into
1443    /// (NOI-5b), capturing the CURRENT Tokio runtime handle to bridge the synchronous
1444    /// connection-process callback onto the async publish.
1445    ///
1446    /// MUST be called from within a Tokio runtime (the server boot path is), so the
1447    /// captured [`Handle`](tokio::runtime::Handle) can spawn the append+fan-out when a
1448    /// worker publishes a transcript event over the reserved channel. Without this
1449    /// builder the observability tap is a no-op (a plain, non-agent deployment).
1450    ///
1451    /// # Panics
1452    ///
1453    /// Panics if called outside a Tokio runtime — a construction-time wiring error in
1454    /// the server boot, never a runtime condition (the boot path always builds the
1455    /// notifier inside the server runtime).
1456    #[must_use]
1457    pub fn with_transcript_publisher(
1458        mut self,
1459        publisher: crate::activity_publisher::ActivityEventPublisher,
1460    ) -> Self {
1461        let (queue, mut events) =
1462            tokio::sync::mpsc::channel::<aion_core::ActivityEvent>(TRANSCRIPT_QUEUE_CAPACITY);
1463        // The ONE drain task: transcript events publish sequentially in
1464        // arrival order (see the `TranscriptTap` docs for why one consumer is
1465        // load-bearing). It lives as long as any queue sender does.
1466        //
1467        // The drain COALESCES under the operator's flush policy: the events
1468        // waiting on this queue are committed as batches, so a chatty agent
1469        // costs one storage-tree commit per batch instead of one per event —
1470        // and one commit is one whole-leaf rewrite (forensics 2026-08-17).
1471        // Arrival order is untouched; only the number of commits changes.
1472        tokio::runtime::Handle::current().spawn(async move {
1473            let dropped = publisher.drain(&mut events, "observability_tap").await;
1474            if dropped > 0 {
1475                tracing::warn!(
1476                    dropped,
1477                    operation = "observability_tap",
1478                    "observability tap: transcript events were not retained"
1479                );
1480            }
1481        });
1482        self.transcript = Some(TranscriptTap { queue });
1483        self
1484    }
1485
1486    /// Set the neutral intervention capability set every worker registering through
1487    /// this notifier advertises (NOI-6, item 4).
1488    ///
1489    /// The composition root wires this from the harness's advertised
1490    /// `AgentSession::capabilities()` so a liminal-connected agent worker's handle
1491    /// carries the primitives its harness supports, which the intervention router
1492    /// gates on. Without this builder the set is empty (observability-only), so a
1493    /// plain activity worker advertises no controls. Pure builder addition, mirroring
1494    /// the registry's capability-carrying registration façade.
1495    #[must_use]
1496    pub fn with_intervention_capabilities(
1497        mut self,
1498        capabilities: aion_core::InterventionCapabilities,
1499    ) -> Self {
1500        self.intervention_capabilities = capabilities;
1501        self
1502    }
1503
1504    /// Bind the connection supervisor the notifier pushes through, immediately
1505    /// after it is constructed with this notifier.
1506    ///
1507    /// Returns `true` when the supervisor was stored, `false` when it was already
1508    /// bound (a second bind is a wiring bug and is ignored, never overwriting the
1509    /// live handle). Call this exactly once, right after
1510    /// [`ConnectionSupervisor::with_services_and_notifier`].
1511    pub fn bind_supervisor(&self, supervisor: ConnectionSupervisor) -> bool {
1512        self.supervisor.set(supervisor).is_ok()
1513    }
1514
1515    /// Snapshot of every live liminal connection the dead-man switch must ping:
1516    /// its pid, the worker it registered, and the push leg to reach it.
1517    ///
1518    /// Built from the SAME guard map that owns each connection's registration,
1519    /// so a connection that has closed (its guard removed) is structurally
1520    /// absent from the snapshot and is never pinged. A poisoned guard map is
1521    /// recovered rather than skipped: silently returning no targets would turn
1522    /// the dead-man switch off exactly when the server is already unhealthy.
1523    #[must_use]
1524    pub fn liveness_targets(&self) -> Vec<super::liminal_liveness::LivenessTarget> {
1525        let Some(supervisor) = self.supervisor.get() else {
1526            return Vec::new();
1527        };
1528        let guards = match self.guards.lock() {
1529            Ok(guards) => guards,
1530            Err(poisoned) => poisoned.into_inner(),
1531        };
1532        guards
1533            .iter()
1534            .filter_map(|(pid, guard)| {
1535                guard
1536                    .worker_id()
1537                    .map(|worker_id| super::liminal_liveness::LivenessTarget {
1538                        pid: *pid,
1539                        worker_id,
1540                        delivery: LiminalWorkerDelivery::new(supervisor.clone(), *pid),
1541                    })
1542            })
1543            .collect()
1544    }
1545
1546    /// Refresh one worker's in-flight task stamp from a [`WorkerLivenessBeat`]
1547    /// published on [`WORKER_LIVENESS_CHANNEL`].
1548    ///
1549    /// The worker is resolved by the publishing CONNECTION (`pid` -> the
1550    /// registration guard this notifier owns), never by wire identity, so a
1551    /// beat can only refresh tasks assigned to the worker that sent it. An
1552    /// untracked task is benign (an outbox dispatch the tracker never held, or
1553    /// a beat racing its completion) and is dropped silently; a malformed
1554    /// payload or unregistered connection is logged and dropped — a bad beat
1555    /// must never tear down the connection callback.
1556    fn record_liveness_beat(&self, pid: u64, payload: &[u8]) {
1557        let Some(tracker) = &self.heartbeat_tracker else {
1558            return;
1559        };
1560        let beat: WorkerLivenessBeat = match serde_json::from_slice(payload) {
1561            Ok(beat) => beat,
1562            Err(error) => {
1563                tracing::warn!(%error, "liveness tap: malformed WorkerLivenessBeat payload");
1564                return;
1565            }
1566        };
1567        let worker_id = match self.guards.lock() {
1568            Ok(guards) => guards.get(&pid).and_then(WorkerRegistration::worker_id),
1569            Err(poisoned) => poisoned
1570                .into_inner()
1571                .get(&pid)
1572                .and_then(WorkerRegistration::worker_id),
1573        };
1574        let Some(worker_id) = worker_id else {
1575            tracing::warn!(
1576                connection_pid = pid,
1577                "liveness tap: beat from a connection with no registered worker"
1578            );
1579            return;
1580        };
1581        let activity_id = ActivityId::from_sequence_position(beat.ordinal);
1582        if let Err(error) = tracker.record_liveness(
1583            worker_id,
1584            &beat.workflow_id,
1585            &activity_id,
1586            std::time::Instant::now(),
1587        ) {
1588            tracing::error!(
1589                %error,
1590                connection_pid = pid,
1591                "liveness tap: heartbeat tracker refresh failed"
1592            );
1593        }
1594    }
1595
1596    /// Apply one worker's [`WorkerCapabilitiesAnnouncement`] published on
1597    /// [`WORKER_CAPABILITIES_CHANNEL`] to its registered handle.
1598    ///
1599    /// The worker is resolved by the publishing CONNECTION (`pid` -> the
1600    /// registration guard this notifier owns), never by wire identity, so an
1601    /// announcement can only ever update the worker that sent it. A malformed
1602    /// payload or unregistered connection is logged and dropped — a bad
1603    /// announcement must never tear down the connection callback.
1604    fn record_capabilities_announcement(&self, pid: u64, payload: &[u8]) {
1605        let announcement: WorkerCapabilitiesAnnouncement = match serde_json::from_slice(payload) {
1606            Ok(announcement) => announcement,
1607            Err(error) => {
1608                tracing::warn!(
1609                    %error,
1610                    "capabilities tap: malformed WorkerCapabilitiesAnnouncement payload"
1611                );
1612                return;
1613            }
1614        };
1615        let worker_id = match self.guards.lock() {
1616            Ok(guards) => guards.get(&pid).and_then(WorkerRegistration::worker_id),
1617            Err(poisoned) => poisoned
1618                .into_inner()
1619                .get(&pid)
1620                .and_then(WorkerRegistration::worker_id),
1621        };
1622        let Some(worker_id) = worker_id else {
1623            tracing::warn!(
1624                connection_pid = pid,
1625                "capabilities tap: announcement from a connection with no registered worker"
1626            );
1627            return;
1628        };
1629        match self
1630            .registry
1631            .set_intervention_capabilities(worker_id, &announcement.capabilities)
1632        {
1633            Ok(true) => {}
1634            Ok(false) => tracing::warn!(
1635                connection_pid = pid,
1636                worker_id = ?worker_id,
1637                "capabilities tap: announcement raced the worker's deregistration"
1638            ),
1639            Err(error) => tracing::error!(
1640                %error,
1641                connection_pid = pid,
1642                "capabilities tap: registry capability update failed"
1643            ),
1644        }
1645    }
1646
1647    fn validate_registration_contract(
1648        &self,
1649        registration: &WireWorkerRegistration,
1650    ) -> Result<(), LiminalServerError> {
1651        let Some(engine) = &self.contract_catalog else {
1652            return Ok(());
1653        };
1654        let advertised =
1655            registration
1656                .activities
1657                .iter()
1658                .map(|activity| {
1659                    let input_schema =
1660                        serde_json::from_str(&activity.input_schema_json).map_err(|error| {
1661                            LiminalServerError::ListenerAccept {
1662                                message: format!(
1663                                    "liminal worker activity `{}` input schema is invalid: {error}",
1664                                    activity.name
1665                                ),
1666                            }
1667                        })?;
1668                    let output_schema = serde_json::from_str(&activity.output_schema_json)
1669                        .map_err(|error| LiminalServerError::ListenerAccept {
1670                            message: format!(
1671                                "liminal worker activity `{}` output schema is invalid: {error}",
1672                                activity.name
1673                            ),
1674                        })?;
1675                    Ok(aion_package::ActivityDescriptor {
1676                        name: activity.name.clone(),
1677                        input_schema,
1678                        output_schema,
1679                    })
1680                })
1681                .collect::<Result<Vec<_>, LiminalServerError>>()?;
1682        // Both advertised forms travel into the gate together: the NAME set the
1683        // dispatcher selects on and the CONTRACT set admission compares. A
1684        // refusal computed from one while the record printed the other is what
1685        // produced the 2026-07-30 self-contradicting log.
1686        let activity_types = registration
1687            .activity_types
1688            .iter()
1689            .cloned()
1690            .collect::<std::collections::BTreeSet<_>>();
1691        super::contracts::validate_worker_contracts(
1692            engine,
1693            self.registry.admission_audit(),
1694            &registration.task_queue,
1695            registration.node.as_deref(),
1696            &registration.identity,
1697            super::contracts::WorkerAdvertisement {
1698                activity_types: &activity_types,
1699                contracts: &advertised,
1700            },
1701        )
1702        .map_err(|error| LiminalServerError::ListenerAccept {
1703            message: error.to_string(),
1704        })
1705    }
1706
1707    /// Admit one connecting worker past the TRANSPORT's own refusals, or refuse
1708    /// it with a structured reason.
1709    ///
1710    /// Split out of [`ConnectionNotifier::on_worker_registered`] so every
1711    /// refusal this transport owns — unbound supervisor, registry error, lease
1712    /// failure — funnels through exactly one place that logs it. Before that
1713    /// split the rejection reason was carried in the `Rejected` ack and logged
1714    /// NOWHERE: on 2026-07-29 a worker whose `.v4` contract field-mismatched was
1715    /// refused on every single dial with the server silent on all of them, and
1716    /// the refused process looked merely asleep.
1717    ///
1718    /// **The contract check is deliberately NOT here.** That 2026-07-29 fix was
1719    /// made in this caller rather than at the shared gate, and within a week the
1720    /// gRPC transport re-manifested the identical silent refusal with nothing in
1721    /// the tree able to notice (#147). The gate now names its own refusal for
1722    /// every transport, so the contract check runs BEFORE this function and its
1723    /// error propagates from here already spoken for.
1724    fn admit_registration(
1725        &self,
1726        pid: u64,
1727        registration: &WireWorkerRegistration,
1728    ) -> Result<(), LiminalServerError> {
1729        // The delivery leg needs the supervisor to push to this connection. In
1730        // correct wiring it is bound before any connection is accepted; a missing
1731        // binding is a rejected registration, never a panic.
1732        let supervisor =
1733            self.supervisor
1734                .get()
1735                .ok_or_else(|| LiminalServerError::ListenerAccept {
1736                    message: format!(
1737                        "liminal worker registration for connection {pid} rejected: \
1738                     notifier supervisor handle not yet bound"
1739                    ),
1740                })?;
1741
1742        let delivery = WorkerDelivery::Liminal(LiminalWorkerDelivery::new(supervisor.clone(), pid));
1743        let node = normalize_wire_node(registration.node.as_deref());
1744        // Insert into the SAME registry, selected the SAME way, as a gRPC worker.
1745        // A registry error (poisoned lock) becomes a Rejected ack so the worker
1746        // never believes it is registered when it is not.
1747        let guard = self
1748            .registry
1749            .register_delivery_with_capabilities(
1750                registration.namespaces.iter().cloned(),
1751                registration.task_queue.clone(),
1752                node,
1753                registration.activity_types.iter(),
1754                delivery,
1755                self.intervention_capabilities.clone(),
1756            )
1757            .map_err(|error| LiminalServerError::ListenerAccept {
1758                message: format!(
1759                    "liminal worker registration for connection {pid} rejected: {error}"
1760                ),
1761            })?;
1762        let worker_id = guard
1763            .worker_id()
1764            .ok_or_else(|| LiminalServerError::ListenerAccept {
1765                message: format!(
1766                    "liminal worker registration for connection {pid} has no worker id"
1767                ),
1768            })?;
1769
1770        // OWN the guard for the connection's lifetime, keyed by pid. Dropping it
1771        // (on unregister) deregisters the worker, so the registration lives
1772        // exactly as long as the connection.
1773        let mut guards = self.guards.lock().map_err(|_| {
1774            // The accepted registry entry cannot be tracked for deregistration, so
1775            // reject (and drop the just-created guard, deregistering it) rather
1776            // than leak a never-deregistered association.
1777            LiminalServerError::ListenerAccept {
1778                message: format!(
1779                    "liminal worker registration for connection {pid} rejected: \
1780                     notifier guard map poisoned"
1781                ),
1782            }
1783        })?;
1784        guards.insert(pid, guard);
1785        drop(guards);
1786        if let Some(tracker) = &self.heartbeat_tracker
1787            && let Err(error) = tracker.register_connection(worker_id, std::time::Instant::now())
1788        {
1789            let removed = match self.guards.lock() {
1790                Ok(mut guards) => guards.remove(&pid),
1791                Err(poisoned) => poisoned.into_inner().remove(&pid),
1792            };
1793            drop(removed);
1794            return Err(LiminalServerError::ListenerAccept {
1795                message: format!(
1796                    "liminal worker registration for connection {pid} rejected: {error}"
1797                ),
1798            });
1799        }
1800        tracing::info!(
1801            connection_pid = pid,
1802            identity = %registration.identity,
1803            task_queue = %registration.task_queue,
1804            "registered liminal worker in-band"
1805        );
1806        Ok(())
1807    }
1808}
1809
1810impl ConnectionNotifier for LiminalConnectionNotifier {
1811    fn on_worker_registered(
1812        &self,
1813        pid: u64,
1814        registration: &WireWorkerRegistration,
1815    ) -> Result<(), LiminalServerError> {
1816        // The contract gate NAMES its own refusal, for every transport, with the
1817        // queue, the node, the worker build identity and the structured reason.
1818        // So this path propagates that refusal SILENTLY rather than restating
1819        // it: a second copy of the same log is how #147 happened, and a log
1820        // written twice per refusal is #94's 12 MB/hour with a second author.
1821        self.validate_registration_contract(registration)?;
1822        // Every refusal this TRANSPORT owns is named here instead, at WARN, with
1823        // the same reason that rides back in the `Rejected` ack, so both sides'
1824        // logs carry identical text and a refused worker can be diagnosed from
1825        // either end.
1826        self.admit_registration(pid, registration)
1827            .inspect_err(|error| {
1828                // The two advertised sets are logged SEPARATELY and named for
1829                // what each one is. Logging `activity_types` alone beside a
1830                // contract-mismatch reason produced a record that listed an
1831                // action as advertised in the same line that reported it
1832                // `<missing>` — true of two different sets, and unresolvable by
1833                // the operator it exists to help.
1834                let advertised_contracts = registration
1835                    .activities
1836                    .iter()
1837                    .map(|activity| activity.name.as_str())
1838                    .collect::<Vec<_>>();
1839                tracing::warn!(
1840                    connection_pid = pid,
1841                    identity = %registration.identity,
1842                    task_queue = %registration.task_queue,
1843                    namespaces = ?registration.namespaces,
1844                    advertised_activity_types = ?registration.activity_types,
1845                    advertised_contracts = ?advertised_contracts,
1846                    reason = %error,
1847                    "REFUSED liminal worker registration"
1848                );
1849            })
1850    }
1851
1852    fn on_worker_unregistered(&self, pid: u64) {
1853        // Remove + drop the guard for pid, deregistering the worker. A poisoned
1854        // lock on the close path has no peer to report to; recover the guard map
1855        // and still drop the guard so the registry does not keep routing to a
1856        // gone connection.
1857        let removed = match self.guards.lock() {
1858            Ok(mut guards) => guards.remove(&pid),
1859            Err(poisoned) => poisoned.into_inner().remove(&pid),
1860        };
1861        let worker_id = removed.as_ref().and_then(WorkerRegistration::worker_id);
1862        if let (Some(tracker), Some(worker_id)) = (&self.heartbeat_tracker, worker_id)
1863            && let Err(error) = tracker.unregister_connection(worker_id)
1864        {
1865            tracing::error!(
1866                %error,
1867                connection_pid = pid,
1868                worker_id = worker_id.value(),
1869                "failed to clear liminal worker connection lease"
1870            );
1871        }
1872        if removed.is_some() {
1873            tracing::info!(
1874                connection_pid = pid,
1875                "deregistered liminal worker on disconnect"
1876            );
1877        }
1878    }
1879
1880    fn on_channel_publish(&self, pid: u64, channel: &str, payload: &[u8]) -> bool {
1881        if let Some(tracker) = &self.heartbeat_tracker {
1882            let worker_id = match self.guards.lock() {
1883                Ok(guards) => guards.get(&pid).and_then(WorkerRegistration::worker_id),
1884                Err(poisoned) => poisoned
1885                    .into_inner()
1886                    .get(&pid)
1887                    .and_then(WorkerRegistration::worker_id),
1888            };
1889            if let Some(worker_id) = worker_id
1890                && let Err(error) =
1891                    tracker.record_connection_activity(worker_id, std::time::Instant::now())
1892            {
1893                tracing::error!(
1894                    %error,
1895                    connection_pid = pid,
1896                    worker_id = worker_id.value(),
1897                    "failed to advance liminal worker connection lease"
1898                );
1899            }
1900        }
1901        // Reserved liveness channel: refresh the beat's in-flight task stamp in
1902        // the shared heartbeat tracker (always consumed, never fanned out).
1903        if channel == WORKER_LIVENESS_CHANNEL {
1904            self.record_liveness_beat(pid, payload);
1905            return true;
1906        }
1907        // Reserved capabilities channel: apply the worker's advertised
1908        // intervention capabilities to its registered handle (always consumed).
1909        if channel == WORKER_CAPABILITIES_CHANNEL {
1910            self.record_capabilities_announcement(pid, payload);
1911            return true;
1912        }
1913        // Only consume the reserved observability channel; any other channel falls
1914        // through to liminal's normal fan-out (this returns false).
1915        if channel != liminal_sdk::OBSERVABILITY_CHANNEL {
1916            return false;
1917        }
1918        let Some(tap) = &self.transcript else {
1919            // No transcript sequencer installed (a non-agent deployment): still
1920            // CONSUME the reserved channel so it never leaks into the fan-out, but
1921            // drop the event — there is nothing to persist it into.
1922            return true;
1923        };
1924        let event: aion_core::ActivityEvent = match serde_json::from_slice(payload) {
1925            Ok(event) => event,
1926            Err(error) => {
1927                tracing::warn!(%error, "observability tap: malformed ActivityEvent payload");
1928                return true;
1929            }
1930        };
1931        // Hand the event to the ordered drain queue (the synchronous
1932        // connection-process callback never blocks). A full queue means the
1933        // durable append cannot keep up: the event is dropped with a warning,
1934        // never buffered without bound.
1935        if let Err(error) = tap.queue.try_send(event) {
1936            tracing::warn!(%error, "observability tap: transcript queue rejected event");
1937        }
1938        true
1939    }
1940}
1941
1942/// The production [`InterventionTransport`](super::intervention::InterventionTransport):
1943/// pushes a routed command to the owning worker over its liminal server-push
1944/// connection (NOI-6, §6.2).
1945///
1946/// It reads the worker handle's [`WorkerDelivery::Liminal`] leg and pushes the
1947/// neutral [`InterventionRequest`] via [`LiminalWorkerDelivery::push_intervention`],
1948/// running the blocking push off the async runtime. A worker delivered over gRPC
1949/// (no liminal leg) surfaces the stale-target no-op via a connection-lost error, so
1950/// the router NACKs the operator rather than routing to a leg it cannot reach.
1951#[derive(Clone, Debug, Default)]
1952pub struct LiminalInterventionTransport;
1953
1954#[async_trait]
1955impl super::intervention::InterventionTransport for LiminalInterventionTransport {
1956    async fn push(
1957        &self,
1958        worker: &super::registry::WorkerHandle,
1959        command: aion_core::InterventionCommand,
1960    ) -> Result<aion_core::InterventionOutcome, ServerError> {
1961        let delivery = match worker.delivery() {
1962            WorkerDelivery::Liminal(delivery) => delivery.clone(),
1963            WorkerDelivery::Grpc(_) => {
1964                // No liminal leg to push to: the intervention transport rides the
1965                // liminal push channel only, so this is unreachable for the target.
1966                return Err(ServerError::worker_connection_lost(
1967                    "liminal-push",
1968                    "owning worker is not delivered over liminal".to_owned(),
1969                ));
1970            }
1971        };
1972        let request = InterventionRequest {
1973            intervention: command,
1974        };
1975        let reply = tokio::task::spawn_blocking(move || delivery.push_intervention(&request))
1976            .await
1977            .map_err(|error| {
1978                dispatch_error(
1979                    "liminal-push",
1980                    format!("intervention task join failed: {error}"),
1981                )
1982            })??;
1983        Ok(reply.outcome)
1984    }
1985}
1986
1987#[cfg(test)]
1988#[path = "liminal_contract_tests.rs"]
1989mod contract_tests;
1990
1991#[cfg(test)]
1992mod tests {
1993    use super::{channel_for_row, dispatch_channel_name, normalize_wire_node};
1994    use aion_core::{ActivityId, ContentType, Payload, WorkflowId};
1995    use aion_store::{OutboxRow, OutboxStatus};
1996    use chrono::Utc;
1997    use uuid::Uuid;
1998
1999    /// The NOI-6 dispatch owner guard RELEASES its binding on drop, on EVERY exit path
2000    /// (reply, error, panic) — so the attempt-owner back-index tracks exactly the
2001    /// attempts currently in flight. This is the invariant the dispatch path relies on
2002    /// to never leak a finished attempt's owner.
2003    #[tokio::test]
2004    async fn attempt_owner_guard_releases_on_drop() -> Result<(), Box<dyn std::error::Error>> {
2005        use super::super::intervention::{AttemptKey, AttemptOwnerIndex};
2006        use super::super::registry::{ConnectedWorkerRegistry, WorkerDelivery};
2007        use super::AttemptOwnerGuard;
2008
2009        // A real registration yields a real WorkerId (there is no fabricated id).
2010        let registry = ConnectedWorkerRegistry::default();
2011        let (tx, _rx) = tokio::sync::mpsc::channel(1);
2012        let types = [String::from("agent")];
2013        let registration = registry.register_delivery_with_capabilities(
2014            [String::from("default")],
2015            String::from("default"),
2016            None,
2017            types.iter(),
2018            WorkerDelivery::Grpc(tx),
2019            aion_core::InterventionCapabilities::none(),
2020        )?;
2021        let worker = registration
2022            .worker_id()
2023            .ok_or("registration must assign a worker id")?;
2024
2025        let owners = AttemptOwnerIndex::new();
2026        let key = AttemptKey::new(
2027            WorkflowId::new(Uuid::nil()),
2028            aion_core::RunId::new(Uuid::from_u128(0x11)),
2029            ActivityId::from_sequence_position(3),
2030            1,
2031        );
2032        owners.bind(key.clone(), worker);
2033        assert_eq!(
2034            owners.owner(&key),
2035            Some(worker),
2036            "owner bound before the guard"
2037        );
2038        {
2039            let _guard = AttemptOwnerGuard {
2040                owners: owners.clone(),
2041                key: key.clone(),
2042            };
2043            assert_eq!(
2044                owners.owner(&key),
2045                Some(worker),
2046                "still bound while in flight"
2047            );
2048        }
2049        // The guard dropped at the end of the block: the binding is released, so a
2050        // later intervention resolves no owner (the too-late no-op).
2051        assert_eq!(
2052            owners.owner(&key),
2053            None,
2054            "owner released when the dispatch returns"
2055        );
2056        Ok(())
2057    }
2058
2059    /// The channel format is pinned EXACTLY: any change is a wire-compatibility
2060    /// break (the dispatcher and any worker subscription must agree byte-for-byte).
2061    /// The UNPINNED (`None`) channel MUST stay byte-identical to the pre-NODE-5
2062    /// format so existing pool subscriptions are stable.
2063    #[test]
2064    fn channel_format_is_pinned() {
2065        assert_eq!(
2066            dispatch_channel_name("remote", "gpu", None),
2067            "aion.dispatch.remote.gpu"
2068        );
2069        assert_eq!(
2070            dispatch_channel_name("local", "norn", None),
2071            "aion.dispatch.local.norn"
2072        );
2073    }
2074
2075    /// A node-pinned dispatch appends the node as an injectively-encoded
2076    /// sub-segment: `f(ns, tq, Some(node))` == `aion.dispatch.{ns}.{tq}.{node}`.
2077    #[test]
2078    fn node_pinned_channel_appends_node_subsegment() {
2079        assert_eq!(
2080            dispatch_channel_name("remote", "gpu", Some("box-7")),
2081            "aion.dispatch.remote.gpu.box-7"
2082        );
2083    }
2084
2085    /// Same input always yields the same channel (the function is stable/total),
2086    /// for both the unpinned and node-pinned cases.
2087    #[test]
2088    fn channel_derivation_is_stable() {
2089        assert_eq!(
2090            dispatch_channel_name("default", "default", None),
2091            dispatch_channel_name("default", "default", None)
2092        );
2093        assert_eq!(
2094            dispatch_channel_name("default", "default", Some("box-1")),
2095            dispatch_channel_name("default", "default", Some("box-1"))
2096        );
2097    }
2098
2099    /// Distinct `(namespace, task_queue)` pools derive distinct channels — the
2100    /// whole point of NSTQ-5: `(remote, gpu)` and `(local, norn)` never collide.
2101    #[test]
2102    fn distinct_pools_get_distinct_channels() {
2103        assert_ne!(
2104            dispatch_channel_name("remote", "gpu", None),
2105            dispatch_channel_name("local", "norn", None)
2106        );
2107    }
2108
2109    /// A node-pinned dispatch and the unpinned dispatch for the SAME pool derive
2110    /// DISTINCT channels, and two distinct nodes for the same pool also differ —
2111    /// the property node isolation rests on (the subscriber contract).
2112    #[test]
2113    fn node_pin_separates_channels() {
2114        let unpinned = dispatch_channel_name("remote", "gpu", None);
2115        let box7 = dispatch_channel_name("remote", "gpu", Some("box-7"));
2116        let box8 = dispatch_channel_name("remote", "gpu", Some("box-8"));
2117        assert_ne!(
2118            unpinned, box7,
2119            "pinned dispatch must not reach unpinned pool"
2120        );
2121        assert_ne!(box7, box8, "distinct nodes must not collide");
2122    }
2123
2124    /// The core injectivity property: free-form fields containing the segment
2125    /// separator `.` must NOT bleed across the join. With the raw `format!` the
2126    /// disjoint pools `("a.b", "c")` and `("a", "b.c")` both collapsed onto
2127    /// `aion.dispatch.a.b.c` — a cross-pool/cross-namespace leak. The per-segment
2128    /// encode keeps them distinct.
2129    #[test]
2130    fn dotted_fields_do_not_collide_across_segments() {
2131        assert_ne!(
2132            dispatch_channel_name("a.b", "c", None),
2133            dispatch_channel_name("a", "b.c", None),
2134            "a '.' in a field must not bleed across the segment separator"
2135        );
2136    }
2137
2138    /// Injectivity holds ACROSS segment counts: a 2-segment (unpinned) channel
2139    /// can never be confused with a 3-segment (node-pinned) channel even when a
2140    /// `.` in a field would otherwise make the raw strings line up. Both
2141    /// directions of the brief's collision cases must stay distinct.
2142    #[test]
2143    fn node_subsegment_does_not_collide_with_dotted_fields() {
2144        // A node sub-segment vs the same dot living inside task_queue.
2145        assert_ne!(
2146            dispatch_channel_name("a", "b", Some("c")),
2147            dispatch_channel_name("a", "b.c", None),
2148            "a node sub-segment must not collide with a dotted task_queue"
2149        );
2150        // The dot living inside namespace vs a node sub-segment.
2151        assert_ne!(
2152            dispatch_channel_name("a.b", "c", None),
2153            dispatch_channel_name("a", "b", Some("c")),
2154            "a dotted namespace must not collide with a node-pinned channel"
2155        );
2156    }
2157
2158    /// More reserved-char shifts that the raw `format!` collapsed but the encode
2159    /// must keep distinct — the dot can sit on either side of the boundary.
2160    #[test]
2161    fn reserved_char_shifts_stay_distinct() {
2162        // Dot at the end of namespace vs start of task_queue.
2163        assert_ne!(
2164            dispatch_channel_name("ns.", "tq", None),
2165            dispatch_channel_name("ns", ".tq", None)
2166        );
2167        // Empty field vs the dot living in the other field.
2168        assert_ne!(
2169            dispatch_channel_name("", "a.b", None),
2170            dispatch_channel_name(".a", "b", None)
2171        );
2172        // The escape char itself must not let a literal `%2E` impersonate an
2173        // encoded `.`: `("%2E", "x")` (literal percent-two-E) must differ from
2174        // `(".", "x")` (an actual dot, which encodes to `%2E`).
2175        assert_ne!(
2176            dispatch_channel_name("%2E", "x", None),
2177            dispatch_channel_name(".", "x", None)
2178        );
2179    }
2180
2181    /// Encoding is injective in ALL THREE segments independently and is exactly
2182    /// reversible (the property the channel relies on), so a small exhaustive
2183    /// sweep of reserved-char arrangements — INCLUDING the optional node taking
2184    /// `None` and every reserved-char value — yields all-distinct channels. This
2185    /// covers cross-segment-count collisions (the `None` vs `Some` boundary) too.
2186    #[test]
2187    fn encoding_is_injective_over_reserved_char_triples() {
2188        let fields = ["a", "a.b", "a.", ".a", ".", "", "%", "%2E", "a%b", "%2."];
2189        let nodes = [
2190            None,
2191            Some("a"),
2192            Some("a.b"),
2193            Some("."),
2194            Some(""),
2195            Some("%2E"),
2196        ];
2197        let mut channels = std::collections::HashSet::new();
2198        for ns in fields {
2199            for tq in fields {
2200                for node in nodes {
2201                    let channel = dispatch_channel_name(ns, tq, node);
2202                    assert!(
2203                        channels.insert(channel.clone()),
2204                        "collision on ({ns:?}, {tq:?}, {node:?}) -> {channel}"
2205                    );
2206                }
2207            }
2208        }
2209    }
2210
2211    fn row(namespace: &str, task_queue: &str) -> OutboxRow {
2212        let workflow_id = WorkflowId::new(Uuid::new_v4());
2213        OutboxRow {
2214            dispatch_key: format!("{workflow_id}:0"),
2215            workflow_id,
2216            ordinal: 0,
2217            run_id: Some(aion_core::RunId::new_v4()),
2218            namespace: namespace.to_owned(),
2219            task_queue: task_queue.to_owned(),
2220            node: None,
2221            activity_type: "charge-card".to_owned(),
2222            input: Payload::new(ContentType::Json, Vec::new()),
2223            status: OutboxStatus::Pending,
2224            attempt: 0,
2225            visible_after: Utc::now(),
2226            claimed_at: None,
2227            failure_delivered: false,
2228        }
2229    }
2230
2231    /// A row's channel is derived from its durable `(namespace, task_queue)`
2232    /// columns (NSTQ-2), through the same single derivation function — and
2233    /// `activity_type` does NOT enter the channel. With `node = None` the channel
2234    /// is byte-identical to the pre-NODE-5 2-segment form.
2235    #[test]
2236    fn channel_for_row_uses_namespace_and_task_queue_only() {
2237        let remote_gpu = row("remote", "gpu");
2238        let local_norn = row("local", "norn");
2239        assert_eq!(channel_for_row(&remote_gpu), "aion.dispatch.remote.gpu");
2240        assert_eq!(channel_for_row(&local_norn), "aion.dispatch.local.norn");
2241        assert_ne!(channel_for_row(&remote_gpu), channel_for_row(&local_norn));
2242
2243        // Two rows that differ ONLY in activity_type derive the SAME channel:
2244        // activity_type is matched after delivery, not used to select the pool.
2245        let mut other_activity = row("remote", "gpu");
2246        other_activity.activity_type = "refund".to_owned();
2247        assert_eq!(
2248            channel_for_row(&remote_gpu),
2249            channel_for_row(&other_activity),
2250            "activity_type must not affect the channel"
2251        );
2252    }
2253
2254    /// A row carrying `Some(node)` (NODE-2) derives the node-pinned sub-channel,
2255    /// distinct from the same pool's unpinned channel; a row with `None` derives
2256    /// the 2-segment channel. `channel_for_row` threads `row.node` through the
2257    /// single derivation function.
2258    #[test]
2259    fn channel_for_row_derives_node_subchannel_when_pinned() {
2260        let mut pinned = row("remote", "gpu");
2261        pinned.node = Some("box-7".to_owned());
2262        assert_eq!(channel_for_row(&pinned), "aion.dispatch.remote.gpu.box-7");
2263
2264        let unpinned = row("remote", "gpu");
2265        assert_eq!(channel_for_row(&unpinned), "aion.dispatch.remote.gpu");
2266        assert_ne!(channel_for_row(&pinned), channel_for_row(&unpinned));
2267    }
2268
2269    /// The outbox wire request stamps the row's stored ZERO-based attempt as a
2270    /// ONE-based delivery attempt (zero is malformed on the wire) — the exact
2271    /// stamp the gRPC outbox arm's `to_scheduled` applies — carries no labels,
2272    /// and assigns no liveness window (outbox rows are not tracker-tracked; the
2273    /// outbox retry loop is their liveness backstop).
2274    #[test]
2275    fn request_for_row_stamps_one_based_attempt_and_no_window() -> Result<(), crate::ServerError> {
2276        let mut retried = row("remote", "gpu");
2277        retried.attempt = 2;
2278        let run_id = retried
2279            .run_id
2280            .as_ref()
2281            .ok_or_else(|| crate::ServerError::worker_dispatch("", "", "test row missing run"))?;
2282        let token = super::CompletionToken::for_test();
2283        let request = super::request_for_row(&retried, run_id, &token);
2284        assert_eq!(
2285            request.attempt, 3,
2286            "zero-based row attempt goes one-based on the wire"
2287        );
2288        assert!(request.labels.is_empty());
2289        assert_eq!(request.heartbeat_window_ms, 0);
2290
2291        let fresh = row("remote", "gpu");
2292        let fresh_run = fresh
2293            .run_id
2294            .as_ref()
2295            .ok_or_else(|| crate::ServerError::worker_dispatch("", "", "test row missing run"))?;
2296        assert_eq!(super::request_for_row(&fresh, fresh_run, &token).attempt, 1);
2297        Ok(())
2298    }
2299
2300    /// The wire `node` is normalized onto the registry's optional affinity with
2301    /// the SAME none-convention the gRPC registration path uses: `None` and the
2302    /// empty-string node both collapse to unpinned (`None`), a non-empty value is
2303    /// the advertised node. An empty-string node must NOT register a distinct
2304    /// empty affinity no pinned dispatch could match.
2305    #[test]
2306    fn wire_node_normalizes_empty_to_none() {
2307        assert_eq!(normalize_wire_node(None), None);
2308        assert_eq!(normalize_wire_node(Some("")), None);
2309        assert_eq!(normalize_wire_node(Some("box-7")), Some("box-7".to_owned()));
2310    }
2311
2312    // --- #163: the Prefer two-tier spill on the LIMINAL selection path ---------
2313    //
2314    // These exercise `RegistryLiminalDispatch::select_liminal_worker` — the
2315    // liminal transport's worker selection — proving it consults the SAME shared
2316    // `preferred_node_order` two-tier spill the gRPC path uses (the cross-node
2317    // demo behaviour), and that placement NEVER mutates the recorded row's node.
2318    // Selection is delivery-agnostic (`select_worker` filters by node regardless
2319    // of transport), so a worker registered with any delivery drives the same
2320    // selection the production liminal-delivered worker would; the tests assert on
2321    // the SELECTED handle's node, which is exactly what #163 changed.
2322    mod placement_selection {
2323        use std::collections::BTreeSet;
2324        use std::sync::Arc;
2325        use std::time::Duration;
2326
2327        use aion_core::{ActivityId, Payload, RunId, WorkflowId};
2328        use aion_store::{
2329            InMemoryStore, NamespaceOrigin, NamespacePlacement, NamespaceStore, OutboxRow,
2330        };
2331
2332        use crate::error::ServerError;
2333        use crate::worker::bridge::OutboxDeliveryCallback;
2334        use crate::worker::registry::{ConnectedWorkerRegistry, WorkerMessage, WorkerRegistration};
2335        use crate::worker::{DeliveryGate, PlacementCache};
2336
2337        use super::super::RegistryLiminalDispatch;
2338
2339        /// No-op delivery callback: the selection tests never deliver a result, so
2340        /// the completion sink is never invoked. Both methods are unreachable in
2341        /// these tests and simply report "no live run" if ever called.
2342        struct NoopCallback;
2343
2344        impl OutboxDeliveryCallback for NoopCallback {
2345            fn deliver_completion(
2346                &self,
2347                _workflow_id: &WorkflowId,
2348                _activity_id: &ActivityId,
2349                _run_id: Option<&RunId>,
2350                _result: String,
2351            ) -> Result<bool, ServerError> {
2352                Ok(false)
2353            }
2354            fn deliver_failure(
2355                &self,
2356                _workflow_id: &WorkflowId,
2357                _activity_id: &ActivityId,
2358                _run_id: Option<&RunId>,
2359                _reason: String,
2360            ) -> Result<bool, ServerError> {
2361                Ok(false)
2362            }
2363        }
2364
2365        fn labels(values: &[&str]) -> BTreeSet<String> {
2366            values.iter().map(|v| (*v).to_owned()).collect()
2367        }
2368
2369        /// Register a worker advertising `node` for `charge` in `namespace`,
2370        /// returning the registration guard (held to keep it connected).
2371        fn register_node_worker(
2372            registry: &ConnectedWorkerRegistry,
2373            namespace: &str,
2374            node: &str,
2375        ) -> Result<WorkerRegistration, ServerError> {
2376            let (tx, _rx) = tokio::sync::mpsc::channel::<WorkerMessage>(1);
2377            let types = [String::from("charge")];
2378            registry.register_namespaces(
2379                [namespace.to_owned()],
2380                String::from("default"),
2381                Some(node.to_owned()),
2382                types.iter(),
2383                tx,
2384            )
2385        }
2386
2387        /// Register a worker carrying NO node label (`node == None`) — the
2388        /// any-node worker a `Prefer` tier spills to and a `Pinned` tier never
2389        /// admits. The distinction is the whole subject of the refusal tests
2390        /// below.
2391        fn register_unlabelled_worker(
2392            registry: &ConnectedWorkerRegistry,
2393            namespace: &str,
2394        ) -> Result<WorkerRegistration, ServerError> {
2395            let (tx, _rx) = tokio::sync::mpsc::channel::<WorkerMessage>(1);
2396            let types = [String::from("charge")];
2397            registry.register_namespaces(
2398                [namespace.to_owned()],
2399                String::from("default"),
2400                None,
2401                types.iter(),
2402                tx,
2403            )
2404        }
2405
2406        /// Publish `worker` as dispatch-ineligible, exactly as a liveness round
2407        /// would.
2408        fn exclude(
2409            registry: &ConnectedWorkerRegistry,
2410            worker: &WorkerRegistration,
2411        ) -> Result<(), Box<dyn std::error::Error>> {
2412            let worker_id = worker
2413                .worker_id()
2414                .ok_or("registration assigned no worker id")?;
2415            registry.set_dispatch_ineligible([worker_id].into_iter().collect())?;
2416            Ok(())
2417        }
2418
2419        /// Drive the REAL dispatch path and hand back the refusal's reason.
2420        ///
2421        /// Through `OutboxRowDispatch::dispatch`, not through the reason
2422        /// function directly: the reason is a STRING an operator reads, and a
2423        /// test that called the private helper would leave the wiring between
2424        /// selection and refusal — the exact seam that got the tiers wrong —
2425        /// uncovered.
2426        async fn refusal_reason(
2427            dispatch: &RegistryLiminalDispatch,
2428            row: &OutboxRow,
2429        ) -> Result<String, Box<dyn std::error::Error>> {
2430            match super::super::OutboxRowDispatch::dispatch(dispatch, row).await {
2431                Ok(()) => Err("the dispatch must be refused: no worker is selectable".into()),
2432                Err(ServerError::WorkerDispatch { reason, .. }) => Ok(reason),
2433                Err(other) => {
2434                    Err(format!("expected a worker-dispatch refusal, got: {other}").into())
2435                }
2436            }
2437        }
2438
2439        /// Build an UNPINNED outbox row (`node == None`) in `namespace` for `charge`.
2440        fn unpinned_row(namespace: &str) -> OutboxRow {
2441            OutboxRow::pending(
2442                WorkflowId::new_v4(),
2443                0,
2444                String::from("charge"),
2445                Payload::from_json(&serde_json::json!({}))
2446                    .unwrap_or_else(|_| Payload::new(aion_core::ContentType::Json, Vec::new())),
2447                chrono::Utc::now(),
2448            )
2449            .with_namespace(namespace)
2450            .with_task_queue("default")
2451        }
2452
2453        /// A namespace store with `namespace` set to `Prefer{nodes}`.
2454        async fn prefer_store(
2455            namespace: &str,
2456            nodes: &[&str],
2457        ) -> Result<Arc<dyn NamespaceStore>, ServerError> {
2458            let store: Arc<dyn NamespaceStore> = Arc::new(InMemoryStore::default());
2459            store
2460                .register_namespace(namespace, NamespaceOrigin::Explicit)
2461                .await?;
2462            store
2463                .set_namespace_placement(
2464                    namespace,
2465                    NamespacePlacement::Prefer {
2466                        nodes: labels(nodes),
2467                    },
2468                )
2469                .await?;
2470            Ok(store)
2471        }
2472
2473        /// A namespace store with `namespace` set to `Pinned{nodes}` (P2-I1).
2474        async fn pinned_store(
2475            namespace: &str,
2476            nodes: &[&str],
2477        ) -> Result<Arc<dyn NamespaceStore>, ServerError> {
2478            let store: Arc<dyn NamespaceStore> = Arc::new(InMemoryStore::default());
2479            store
2480                .register_namespace(namespace, NamespaceOrigin::Explicit)
2481                .await?;
2482            store
2483                .set_namespace_placement(
2484                    namespace,
2485                    NamespacePlacement::Pinned {
2486                        nodes: labels(nodes),
2487                    },
2488                )
2489                .await?;
2490            Ok(store)
2491        }
2492
2493        /// Build a `RegistryLiminalDispatch` over `registry` whose placement cache
2494        /// reads `ns_store` (zero TTL so each selection sees the latest placement).
2495        fn liminal_dispatch(
2496            registry: &ConnectedWorkerRegistry,
2497            ns_store: Arc<dyn NamespaceStore>,
2498        ) -> RegistryLiminalDispatch {
2499            let cache = PlacementCache::new(ns_store, Duration::ZERO);
2500            RegistryLiminalDispatch::new(
2501                registry.clone(),
2502                Arc::new(NoopCallback),
2503                DeliveryGate::default(),
2504            )
2505            .with_placement_cache(cache)
2506        }
2507
2508        /// R3 (#197): a pool whose only worker is eligibility-excluded says SO,
2509        /// and does not report itself empty.
2510        ///
2511        /// The remedies differ and are close to opposite: an empty pool needs a
2512        /// worker started, an excluded pool needs its liveness verdict read —
2513        /// and starting a second worker there changes nothing, because the new
2514        /// one serves the same probation.
2515        #[tokio::test]
2516        async fn an_excluded_pool_refusal_names_the_exclusion()
2517        -> Result<(), Box<dyn std::error::Error>> {
2518            let registry = ConnectedWorkerRegistry::default();
2519            let worker = register_unlabelled_worker(&registry, "t")?;
2520            exclude(&registry, &worker)?;
2521            let dispatch = RegistryLiminalDispatch::new(
2522                registry.clone(),
2523                Arc::new(NoopCallback),
2524                DeliveryGate::default(),
2525            );
2526
2527            let reason = refusal_reason(&dispatch, &unpinned_row("t")).await?;
2528            assert!(
2529                reason.contains("NONE is currently dispatch-eligible"),
2530                "the refusal must name the exclusion it found; said: {reason}"
2531            );
2532            assert!(
2533                !reason.contains("no liminal worker registered"),
2534                "a pool holding a registered worker is not an empty pool; said: {reason}"
2535            );
2536            Ok(())
2537        }
2538
2539        /// THE CONTROL for the test above: a genuinely empty pool still reports
2540        /// itself empty.
2541        ///
2542        /// Without it a refusal hard-coded to the exclusion sentence would
2543        /// satisfy that test and misdiagnose every truly unserved queue,
2544        /// telling an operator not to start the worker they need.
2545        #[tokio::test]
2546        async fn an_empty_pool_refusal_says_nobody_is_registered()
2547        -> Result<(), Box<dyn std::error::Error>> {
2548            let registry = ConnectedWorkerRegistry::default();
2549            let dispatch = RegistryLiminalDispatch::new(
2550                registry.clone(),
2551                Arc::new(NoopCallback),
2552                DeliveryGate::default(),
2553            );
2554
2555            let reason = refusal_reason(&dispatch, &unpinned_row("t")).await?;
2556            assert!(
2557                reason.contains("no liminal worker registered for the row\'s pool"),
2558                "an empty pool must say nobody is registered; said: {reason}"
2559            );
2560            assert!(
2561                !reason.contains("starting another worker will not help"),
2562                "starting a worker is EXACTLY the remedy for an empty pool; said: {reason}"
2563            );
2564            Ok(())
2565        }
2566
2567        /// 🔴 F3: a `Pinned{{n1}}` namespace with no n1-labelled worker must not
2568        /// be blamed on an ineligible UNLABELLED worker.
2569        ///
2570        /// Selection walked `Required{{n1}}` — the unlabelled worker was never a
2571        /// candidate, because a hard pin has no `None` spill. Counting the
2572        /// exclusion over the row\'s own node instead (`None`, which matches
2573        /// EVERY worker in the pool) makes the refusal say "starting another
2574        /// worker will not help" when starting an n1-labelled worker is the one
2575        /// thing that would.
2576        ///
2577        /// A refusal that sends an operator away from the only remedy is worse
2578        /// than the catch-all it replaced, so the count must be taken over the
2579        /// SAME tier sequence selection actually walked.
2580        #[tokio::test]
2581        async fn a_pinned_namespace_miss_is_not_blamed_on_an_ineligible_unlabelled_worker()
2582        -> Result<(), Box<dyn std::error::Error>> {
2583            let ns_store = pinned_store("t", &["n1"]).await?;
2584            let registry = ConnectedWorkerRegistry::default();
2585            // The only worker in the pool: unlabelled, and excluded. No
2586            // n1-labelled worker exists at all.
2587            let worker = register_unlabelled_worker(&registry, "t")?;
2588            exclude(&registry, &worker)?;
2589            let dispatch = liminal_dispatch(&registry, Arc::clone(&ns_store));
2590
2591            let reason = refusal_reason(&dispatch, &unpinned_row("t")).await?;
2592            assert!(
2593                !reason.contains("starting another worker will not help"),
2594                "the excluded worker was never admissible for a hard pin, so it cannot be the \
2595                 reason this row found nobody — and an n1-labelled worker IS the remedy this \
2596                 refusal just told the operator not to try. Said: {reason}"
2597            );
2598            assert!(
2599                reason.contains("no liminal worker registered for the row\'s pool"),
2600                "no worker admissible to the pin is registered, and that is what the refusal \
2601                 must say; said: {reason}"
2602            );
2603            Ok(())
2604        }
2605
2606        /// THE CONTROL for the pin test: with an n1-labelled worker present and
2607        /// excluded, the SAME namespace does report the exclusion.
2608        ///
2609        /// Without it, a count that always returned zero under a pin would
2610        /// satisfy the test above and silently retire the whole R3 distinction
2611        /// for every pinned namespace.
2612        #[tokio::test]
2613        async fn a_pinned_namespace_does_report_an_excluded_admissible_worker()
2614        -> Result<(), Box<dyn std::error::Error>> {
2615            let ns_store = pinned_store("t", &["n1"]).await?;
2616            let registry = ConnectedWorkerRegistry::default();
2617            let worker = register_node_worker(&registry, "t", "n1")?;
2618            exclude(&registry, &worker)?;
2619            let dispatch = liminal_dispatch(&registry, Arc::clone(&ns_store));
2620
2621            let reason = refusal_reason(&dispatch, &unpinned_row("t")).await?;
2622            assert!(
2623                reason.contains("NONE is currently dispatch-eligible"),
2624                "an n1-labelled worker IS admissible to Pinned{{n1}}, so its exclusion is exactly \
2625                 why this row found nobody; said: {reason}"
2626            );
2627            Ok(())
2628        }
2629
2630        /// T3 — the finding on the liminal path: two workers sharing ONE tier.
2631        ///
2632        /// This is the shape every `aion worker agent --liminal-address` fleet
2633        /// takes on a host that runs more than one worker: both are on the same
2634        /// node, both admissible to `Prefer{n1}`'s first tier, and selection used
2635        /// to hand every outbox row to the lower worker id while the other idled.
2636        /// The tier itself is untouched — the walk still stops at the first tier
2637        /// with a live worker; what changed is WHICH of that tier's eligible
2638        /// workers is chosen.
2639        #[tokio::test]
2640        async fn prefer_tier_rotates_across_workers_on_the_same_node()
2641        -> Result<(), Box<dyn std::error::Error>> {
2642            let ns_store = prefer_store("t", &["n1"]).await?;
2643            let registry = ConnectedWorkerRegistry::default();
2644            let first = register_node_worker(&registry, "t", "n1")?;
2645            let second = register_node_worker(&registry, "t", "n1")?;
2646            let first_id = first
2647                .worker_id()
2648                .ok_or("registration assigned no worker id")?;
2649            let second_id = second
2650                .worker_id()
2651                .ok_or("registration assigned no worker id")?;
2652            let dispatch = liminal_dispatch(&registry, Arc::clone(&ns_store));
2653
2654            let mut selected = Vec::new();
2655            for _ in 0..4 {
2656                let row = unpinned_row("t");
2657                let worker = dispatch
2658                    .select_liminal_worker(&row)
2659                    .await?
2660                    .worker
2661                    .ok_or("a live n1 worker must be selected")?;
2662                assert_eq!(
2663                    worker.node(),
2664                    Some("n1"),
2665                    "both workers sit on the preferred tier; the spill is never reached"
2666                );
2667                selected.push(worker.id());
2668            }
2669            assert_eq!(
2670                selected,
2671                vec![first_id, second_id, first_id, second_id],
2672                "the Prefer{{n1}} tier must rotate across BOTH n1 workers, not pin every row to \
2673                 the lower worker id"
2674            );
2675            Ok(())
2676        }
2677
2678        /// #163 (prefer): an unpinned row in a `Prefer{n1}` namespace selects the
2679        /// n1 worker on the liminal path when one is live, even with an n2 worker
2680        /// also connected.
2681        #[tokio::test]
2682        async fn prefer_selects_preferred_node_worker_on_liminal_path()
2683        -> Result<(), Box<dyn std::error::Error>> {
2684            let ns_store = prefer_store("t", &["n1"]).await?;
2685            let registry = ConnectedWorkerRegistry::default();
2686            let _n1 = register_node_worker(&registry, "t", "n1")?;
2687            let _n2 = register_node_worker(&registry, "t", "n2")?;
2688            let dispatch = liminal_dispatch(&registry, Arc::clone(&ns_store));
2689
2690            let row = unpinned_row("t");
2691            let selected = dispatch
2692                .select_liminal_worker(&row)
2693                .await?
2694                .worker
2695                .ok_or("a worker must be selected")?;
2696            assert_eq!(
2697                selected.node(),
2698                Some("n1"),
2699                "the liminal path prefers the n1 worker while it is live"
2700            );
2701            // Determinism gate: preference never mutates the recorded row's node.
2702            assert_eq!(row.node, None, "placement must never mutate the row's node");
2703            Ok(())
2704        }
2705
2706        /// #163 (spill): an unpinned row in a `Prefer{n1}` namespace SPILLS to the
2707        /// only live worker (n2) on the liminal path when no n1 worker is
2708        /// connected — the cross-node node-loss failover behaviour.
2709        #[tokio::test]
2710        async fn prefer_spills_to_any_live_worker_on_liminal_path()
2711        -> Result<(), Box<dyn std::error::Error>> {
2712            let ns_store = prefer_store("t", &["n1"]).await?;
2713            // Only an n2 worker is live: no n1-labelled worker exists at all.
2714            let registry = ConnectedWorkerRegistry::default();
2715            let _n2 = register_node_worker(&registry, "t", "n2")?;
2716            let dispatch = liminal_dispatch(&registry, Arc::clone(&ns_store));
2717
2718            let row = unpinned_row("t");
2719            let selected = dispatch
2720                .select_liminal_worker(&row)
2721                .await?
2722                .worker
2723                .ok_or("the spill must select the live n2 worker")?;
2724            assert_eq!(
2725                selected.node(),
2726                Some("n2"),
2727                "with no n1 worker live, the liminal selection spills to the live n2 worker"
2728            );
2729            assert_eq!(row.node, None, "spill must never mutate the row's node");
2730            Ok(())
2731        }
2732
2733        /// #163 (determinism, mirrors the gRPC `placement_never_mutates_recorded_row_node`
2734        /// test): under `Prefer{n1}` the SAME unpinned row selected once to the n1
2735        /// worker and once (after n1 leaves) spilled to n2 keeps `node == None`
2736        /// BOTH times — selection reads the row's node, never the placement, so
2737        /// replay sees an identical command stream irrespective of the target.
2738        #[tokio::test]
2739        async fn placement_never_mutates_recorded_row_node_on_liminal_path()
2740        -> Result<(), Box<dyn std::error::Error>> {
2741            let ns_store = prefer_store("t", &["n1"]).await?;
2742            let registry = ConnectedWorkerRegistry::default();
2743            let dispatch = liminal_dispatch(&registry, Arc::clone(&ns_store));
2744
2745            // Routing A: n1 present -> preferred selection.
2746            let n1 = register_node_worker(&registry, "t", "n1")?;
2747            let row_a = unpinned_row("t");
2748            let selected_a = dispatch
2749                .select_liminal_worker(&row_a)
2750                .await?
2751                .worker
2752                .ok_or("routing A must select a worker")?;
2753            assert_eq!(selected_a.node(), Some("n1"));
2754
2755            // n1 leaves; only n2 remains.
2756            n1.deregister()?;
2757            let _n2 = register_node_worker(&registry, "t", "n2")?;
2758
2759            // Routing B: same shape of unpinned row -> spills to n2.
2760            let row_b = unpinned_row("t");
2761            let selected_b = dispatch
2762                .select_liminal_worker(&row_b)
2763                .await?
2764                .worker
2765                .ok_or("routing B must spill to a worker")?;
2766            assert_eq!(selected_b.node(), Some("n2"));
2767
2768            // The recorded row node is None in BOTH routings: the dispatch target
2769            // (n1 vs n2) did not perturb it.
2770            assert_eq!(row_a.node, None);
2771            assert_eq!(row_b.node, None);
2772            assert_eq!(
2773                row_a.node, row_b.node,
2774                "the recorded row node is identical regardless of which worker was selected"
2775            );
2776            Ok(())
2777        }
2778
2779        /// #164 (P2-I1 hard pin): an unpinned row in a `Pinned{n1}` namespace
2780        /// selects the n1 worker on the liminal path when live — exactly like
2781        /// Prefer's happy path.
2782        #[tokio::test]
2783        async fn pinned_selects_required_node_worker_on_liminal_path()
2784        -> Result<(), Box<dyn std::error::Error>> {
2785            let ns_store = pinned_store("t", &["n1"]).await?;
2786            let registry = ConnectedWorkerRegistry::default();
2787            let _n1 = register_node_worker(&registry, "t", "n1")?;
2788            let _n2 = register_node_worker(&registry, "t", "n2")?;
2789            let dispatch = liminal_dispatch(&registry, Arc::clone(&ns_store));
2790
2791            let row = unpinned_row("t");
2792            let selected = dispatch
2793                .select_liminal_worker(&row)
2794                .await?
2795                .worker
2796                .ok_or("the required n1 worker must be selected")?;
2797            assert_eq!(selected.node(), Some("n1"));
2798            assert_eq!(row.node, None, "placement must never mutate the row's node");
2799            Ok(())
2800        }
2801
2802        /// #164 (P2-I1 no spill — the load-bearing test): an unpinned row in a
2803        /// `Pinned{n1}` namespace with ONLY a live n2 worker selects NOTHING — it
2804        /// must NEVER spill to the wrong-node worker. This is the exact opposite of
2805        /// the `prefer_spills_to_any_live_worker_on_liminal_path` behaviour and would
2806        /// FAIL under the old fall-through (which selected any worker for Pinned).
2807        /// The `Ok(None)` drives the outbox no-worker retry/stall, mirroring the
2808        /// gRPC wait.
2809        #[tokio::test]
2810        async fn pinned_never_spills_to_a_wrong_node_worker_on_liminal_path()
2811        -> Result<(), Box<dyn std::error::Error>> {
2812            let ns_store = pinned_store("t", &["n1"]).await?;
2813            // Only an n2 worker is live: no n1-labelled worker exists at all.
2814            let registry = ConnectedWorkerRegistry::default();
2815            let _n2 = register_node_worker(&registry, "t", "n2")?;
2816            let dispatch = liminal_dispatch(&registry, Arc::clone(&ns_store));
2817
2818            let row = unpinned_row("t");
2819            let selected = dispatch.select_liminal_worker(&row).await?.worker;
2820            assert!(
2821                selected.is_none(),
2822                "Pinned{{n1}} must NOT spill to the live n2 worker — it selects nothing \
2823                 so the outbox retries/stalls until an n1 worker returns"
2824            );
2825            assert_eq!(row.node, None, "placement must never mutate the row's node");
2826            Ok(())
2827        }
2828
2829        /// #163 (authored pin wins): a row authored-pinned to `Some(n2)` STILL
2830        /// selects an n2 worker on the liminal path regardless of the namespace's
2831        /// `Prefer{n1}` — the per-activity pin is authoritative and placement never
2832        /// overrides it.
2833        #[tokio::test]
2834        async fn authored_node_pin_wins_over_namespace_prefer_on_liminal_path()
2835        -> Result<(), Box<dyn std::error::Error>> {
2836            let ns_store = prefer_store("t", &["n1"]).await?;
2837            let registry = ConnectedWorkerRegistry::default();
2838            let _n1 = register_node_worker(&registry, "t", "n1")?;
2839            let _n2 = register_node_worker(&registry, "t", "n2")?;
2840            let dispatch = liminal_dispatch(&registry, Arc::clone(&ns_store));
2841
2842            // Authored pin: node = Some("n2").
2843            let row = unpinned_row("t").with_node(Some(String::from("n2")));
2844            let selected = dispatch
2845                .select_liminal_worker(&row)
2846                .await?
2847                .worker
2848                .ok_or("the authored pin must select the n2 worker")?;
2849            assert_eq!(
2850                selected.node(),
2851                Some("n2"),
2852                "the authored Some(n2) pin is honoured regardless of the namespace Prefer{{n1}}"
2853            );
2854            // The authored node is preserved exactly (determinism gate).
2855            assert_eq!(row.node.as_deref(), Some("n2"));
2856            Ok(())
2857        }
2858
2859        /// #163 (byte-identical default): an `Unplaced` namespace selects any live
2860        /// worker on the liminal path exactly as the pre-Phase-2 single
2861        /// `select_worker` would — the ceiling/placement never engages.
2862        #[tokio::test]
2863        async fn unplaced_namespace_selects_any_worker_on_liminal_path()
2864        -> Result<(), Box<dyn std::error::Error>> {
2865            let ns_store: Arc<dyn NamespaceStore> = Arc::new(InMemoryStore::default());
2866            // Registered but left Unplaced (the default placement).
2867            ns_store
2868                .register_namespace("t", NamespaceOrigin::Explicit)
2869                .await?;
2870            let registry = ConnectedWorkerRegistry::default();
2871            let _n2 = register_node_worker(&registry, "t", "n2")?;
2872            let dispatch = liminal_dispatch(&registry, Arc::clone(&ns_store));
2873
2874            let selected = dispatch
2875                .select_liminal_worker(&unpinned_row("t"))
2876                .await?
2877                .worker
2878                .ok_or("an Unplaced namespace still selects a live worker")?;
2879            assert_eq!(
2880                selected.node(),
2881                Some("n2"),
2882                "an Unplaced namespace reaches any live worker, exactly as before"
2883            );
2884            Ok(())
2885        }
2886
2887        /// #163 (byte-identical, no cache): with NO placement cache attached, the
2888        /// liminal selection is the single `select_worker` off the row's own node —
2889        /// byte-identical to the pre-#163 construction. An unpinned row reaches any
2890        /// live worker; the namespace's `Prefer` is not even consulted.
2891        #[tokio::test]
2892        async fn no_cache_selection_is_byte_identical_to_pre_163()
2893        -> Result<(), Box<dyn std::error::Error>> {
2894            // The namespace prefers n1, but with no cache the preference is ignored.
2895            let _ns_store = prefer_store("t", &["n1"]).await?;
2896            let registry = ConnectedWorkerRegistry::default();
2897            let _n2 = register_node_worker(&registry, "t", "n2")?;
2898            // No `.with_placement_cache(...)`: the pre-#163 construction.
2899            let dispatch = RegistryLiminalDispatch::new(
2900                registry.clone(),
2901                Arc::new(NoopCallback),
2902                DeliveryGate::default(),
2903            );
2904
2905            let selected = dispatch
2906                .select_liminal_worker(&unpinned_row("t"))
2907                .await?
2908                .worker
2909                .ok_or("without a cache the unpinned row still selects any worker")?;
2910            assert_eq!(
2911                selected.node(),
2912                Some("n2"),
2913                "with no placement cache the selection is the unchanged any-worker path"
2914            );
2915            Ok(())
2916        }
2917    }
2918}