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//! - [`LiminalWorkerDelivery`] is the per-worker push handle this module hands
30//!   to the registry at registration. Delivering over it is
31//!   [`LiminalTaskDelivery`](super::liminal_task_delivery::LiminalTaskDelivery),
32//!   the liminal arm of the shared delivery seam, which pushes the
33//!   [`DispatchRequest`] out on the worker's existing connection and re-enters
34//!   the worker's [`DispatchResponse`] through the SAME
35//!   [`LiminalCompletionSource`] / [`OutboxDeliveryCallback`] the gRPC
36//!   completion path uses.
37//!
38//!   ๐Ÿ”ด This module does NOT select a worker and no longer implements
39//!   [`OutboxRowDispatch`](super::outbox_dispatcher::OutboxRowDispatch). It used
40//!   to, through a `RegistryLiminalDispatch` that ran its own copy of the
41//!   selection โ€” and a server with `outbox.transport = liminal` therefore
42//!   reached EVERY selected worker over liminal, refusing the gRPC-registered
43//!   ones it had just selected. That was #52. Selection now happens once, in
44//!   [`ActivityDispatcher`](super::dispatch::ActivityDispatcher), and each
45//!   chosen worker is served over the transport IT registered on.
46//! - [`LiminalConnectionNotifier`] is the SERVER half of in-band registration:
47//!   when a worker connects with a [`WorkerRegistration`](WireWorkerRegistration)
48//!   the notifier inserts a [`WorkerDelivery::Liminal`] into the registry, and
49//!   drops it on disconnect.
50//! - [`LiminalCompletionSource`] maps a [`DispatchResponse`] onto the delivery
51//!   callback, threading `run_id` end-to-end so the existing continue-as-new run
52//!   gates apply unchanged.
53//!
54//! # The channel-subscription seam (documented, distinct from the push path)
55//!
56//! [`dispatch_channel_name`] derives the pool channel a `(namespace, task_queue)`
57//! pool addresses, optionally pinned to a `node` (NODE-2/NODE-5). The production
58//! path above does not publish to that channel โ€” it pushes to a connected worker
59//! the server already owns โ€” but the derivation is retained as the pinned contract
60//! any future channel-subscription transport MUST honour so the dispatcher and a
61//! subscriber cannot drift:
62//!
63//! - An UNPINNED worker pool addressed `(namespace, task_queue)` subscribes to
64//!   `dispatch_channel_name(namespace, task_queue, None)`.
65//! - A NODE-PINNED dispatch (the row carries `Some(node)`) maps to
66//!   `dispatch_channel_name(namespace, task_queue, Some(node))` โ€” a DISTINCT
67//!   channel that a worker on that node must ALSO subscribe to in order to serve
68//!   pinned work; the unpinned channel alone never delivers a pinned dispatch.
69//!
70//! That is the single contract the seam must honour.
71
72use std::collections::HashMap;
73use std::future::Future;
74use std::sync::{Arc, Mutex, OnceLock};
75use std::time::Duration;
76
77use aion_core::{ActivityId, ContentType, Payload, RunId, WorkflowId};
78use aion_store::OutboxRow;
79use async_trait::async_trait;
80use liminal::protocol::WorkerRegistration as WireWorkerRegistration;
81use liminal_sdk::{SchemaMetadata, SchemaValidate};
82use liminal_server::ServerError as LiminalServerError;
83use liminal_server::server::connection::{
84    ConnectionNotifier, ConnectionSupervisor, PushReplyAwaiter,
85};
86use serde::{Deserialize, Serialize};
87
88use super::bridge::OutboxDeliveryCallback;
89use super::envelope::{CompletionFences, CompletionToken};
90use super::registry::{ConnectedWorkerRegistry, WorkerDelivery, WorkerRegistration};
91use crate::error::ServerError;
92use crate::namespace::{CallerIdentity, NamespaceGuard};
93
94/// Upper bound on how long a server-initiated intervention push waits for the
95/// worker's correlated control-plane acknowledgement. Activity dispatch replies
96/// use an unbounded held wait because an activity may legitimately run a while.
97const PUSH_REPLY_TIMEOUT: Duration = Duration::from_secs(30);
98
99/// Re-arm cadence for the engine-seam bridge's UNBOUNDED reply wait
100/// ([`receive_bridge_reply`]). Each elapsed poll is a benign re-arm, never a
101/// failure: the bridge dispatch contract imposes no activity timeout of its own
102/// (agent-style activities legitimately run for over an hour), exactly like the
103/// gRPC bridge's unbounded `recv`. Worker loss still terminates the wait
104/// promptly โ€” the awaiter wakes with the typed Disconnected error the moment the
105/// connection closes.
106const BRIDGE_REPLY_POLL: Duration = Duration::from_secs(1);
107
108/// Wire request carrying one scheduled activity to a liminal worker.
109///
110/// Mirrors the dispatch half of the gRPC `ActivityTask`: the fields the worker
111/// needs to execute the activity and to correlate its result back to the exact
112/// execution (`workflow_id`, `ordinal`, `run_id`). `run_id` rides end-to-end so
113/// the existing continue-as-new run gates hold over the liminal wire (the design
114/// doc ยง3.3 requirement that `RunId` stays on the wire).
115#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
116pub struct DispatchRequest {
117    /// Activity type the worker must execute.
118    pub activity_type: String,
119    /// Workflow that scheduled this fan-out activity. Carried in its serde form
120    /// so no fragile id parsing happens on the wire.
121    pub workflow_id: WorkflowId,
122    /// Pinned ordinal of this activity within the workflow's fan-out range.
123    pub ordinal: u64,
124    /// Run that dispatched this ordinal, when known (continue-as-new safety).
125    pub run_id: Option<RunId>,
126    /// Opaque execution-generation proof echoed verbatim by the worker.
127    pub completion_token: String,
128    /// Stable external-effect key for this run and action site.
129    pub idempotency_key: String,
130    /// Opaque activity input bytes (JSON-tagged on the aion side), in the one
131    /// payload-bytes wire shape ([`aion_core::payload_bytes`]): a base64 string
132    /// or an array of byte integers decode; the encoder is the codec's.
133    #[serde(with = "aion_core::payload_bytes")]
134    pub input: Vec<u8>,
135    /// One-based delivery attempt, mirroring the gRPC `ActivityTask.attempt`.
136    /// The engine-seam bridge threads the real attempt so a retry executes with
137    /// attempt-aware handler semantics identical to the gRPC transport; the
138    /// outbox path stamps the row's stored zero-based attempt as one-based.
139    /// Serde-defaulted to `1` so a frame from a pre-attempt server (or an old
140    /// recorded frame) still decodes as a first delivery.
141    #[serde(default = "first_attempt")]
142    pub attempt: u32,
143    /// Engine-provided routing/metadata labels, mirroring the gRPC
144    /// `ActivityTask.labels`. Empty (the serde default) on the outbox path,
145    /// which has no label source.
146    #[serde(default)]
147    pub labels: std::collections::BTreeMap<String, String>,
148    /// The server's heartbeat window in milliseconds when this dispatch is
149    /// tracked by the server's per-task liveness tracker (the engine-seam
150    /// bridge path), or `0` when it is not (the outbox path, whose liveness
151    /// backstop is its own retry loop). A non-zero window tells the worker to
152    /// pump automatic liveness beats at a quarter-window cadence so the
153    /// server's heartbeat sweeper never expires a healthy long-running
154    /// activity โ€” the exact liminal mirror of the gRPC worker's automatic
155    /// liveness pump.
156    #[serde(default)]
157    pub heartbeat_window_ms: u64,
158}
159
160/// Serde default for [`DispatchRequest::attempt`]: a frame that predates the
161/// attempt field is a first delivery.
162const fn first_attempt() -> u32 {
163    1
164}
165
166impl SchemaValidate for DispatchRequest {
167    fn schema_metadata() -> SchemaMetadata {
168        SchemaMetadata::new(
169            "aion.outbox.dispatch.request",
170            "1",
171            br#"{"type":"object"}"#.as_slice(),
172        )
173    }
174}
175
176/// Wire response carrying one worker result back to the outbox.
177///
178/// Mirrors the completion half of the gRPC `ActivityResult`: the correlation ids
179/// plus either a success result or a failure reason. `LiminalCompletionSource`
180/// maps this onto the existing [`OutboxDeliveryCallback`].
181#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
182pub struct DispatchResponse {
183    /// Workflow the completion belongs to.
184    pub workflow_id: WorkflowId,
185    /// Pinned ordinal the completion correlates against.
186    pub ordinal: u64,
187    /// Run that issued the dispatch, echoed back for the run gate.
188    pub run_id: Option<RunId>,
189    /// Opaque execution-generation proof echoed from the request.
190    pub completion_token: String,
191    /// Worker outcome: `Ok(result)` or `Err(reason)`.
192    pub outcome: Result<String, String>,
193}
194
195impl SchemaValidate for DispatchResponse {
196    fn schema_metadata() -> SchemaMetadata {
197        SchemaMetadata::new(
198            "aion.outbox.dispatch.response",
199            "1",
200            br#"{"type":"object"}"#.as_slice(),
201        )
202    }
203}
204
205/// Wire request carrying one neutral mid-run intervention command to a liminal
206/// worker (NOI-6, ยง6.2).
207///
208/// Rides the SAME liminal server-push channel as [`DispatchRequest`], distinguished
209/// on the wire by its unique required `intervention` field โ€” a plain
210/// [`DispatchRequest`] has no such field, so the worker demuxes the two by which
211/// one deserializes. The whole envelope is neutral: it carries an
212/// [`InterventionCommand`], never a harness type. Field-for-field mirrored by the
213/// worker's `liminal::InterventionRequest`.
214#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
215pub struct InterventionRequest {
216    /// The neutral command to route to the worker owning the target attempt.
217    pub intervention: aion_core::InterventionCommand,
218}
219
220impl SchemaValidate for InterventionRequest {
221    fn schema_metadata() -> SchemaMetadata {
222        SchemaMetadata::new(
223            "aion.intervention.request",
224            "1",
225            br#"{"type":"object"}"#.as_slice(),
226        )
227    }
228}
229
230/// Wire response carrying the worker's neutral intervention ack back to the server
231/// (NOI-6). Field-for-field mirrored by the worker's `liminal::InterventionReply`.
232#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
233pub struct InterventionReply {
234    /// The neutral applied/gated/stale outcome the operator receives.
235    pub outcome: aion_core::InterventionOutcome,
236}
237
238impl SchemaValidate for InterventionReply {
239    fn schema_metadata() -> SchemaMetadata {
240        SchemaMetadata::new(
241            "aion.intervention.reply",
242            "1",
243            br#"{"type":"object"}"#.as_slice(),
244        )
245    }
246}
247
248/// Reserved liminal channel a worker publishes automatic liveness beats on.
249///
250/// The liminal wire has no gRPC-style heartbeat frame, so per-task liveness
251/// rides a reserved publish channel exactly as the observability transcript
252/// does: the worker's runtime pumps a [`WorkerLivenessBeat`] per in-flight
253/// tracked dispatch at a quarter-window cadence, and the server's
254/// [`LiminalConnectionNotifier`] consumes the channel and refreshes the shared
255/// [`HeartbeatTracker`] โ€” so the #176 expiry sweeper is genuinely
256/// transport-agnostic: a healthy liminal worker running a long activity is
257/// never falsely expired, and a wedged one (which stops pumping) still is.
258/// Mirrored byte-for-byte by the worker crate's constant of the same name.
259pub const WORKER_LIVENESS_CHANNEL: &str = "aion.worker.liveness";
260
261/// Reserved liminal channel a worker announces its capacity and its
262/// intervention capabilities on.
263///
264/// The in-band [`WireWorkerRegistration`] frame is a published liminal protocol
265/// type and cannot carry aion-level metadata, so EVERY liminal worker publishes
266/// a [`WorkerCapabilitiesAnnouncement`] here immediately after registering (once
267/// per connection, so a redialed worker re-announces). The notifier consumes the
268/// channel and applies the announcement to the registered handle: the
269/// `max_concurrency` selection gates dispatch on, and the capability set the
270/// intervention router gates on and the ops console's live-attempts enumeration
271/// reports. Mirrored byte-for-byte by the worker crate's constant of the same
272/// name.
273///
274/// It is unconditional now, where it used to be sent only by a worker whose
275/// harness supported interventions. A worker that stays silent here never
276/// becomes dispatchable, so "nothing to announce" is no longer a reason not to
277/// announce.
278pub const WORKER_CAPABILITIES_CHANNEL: &str = "aion.worker.capabilities";
279
280/// Wire announcement of a worker's advertised intervention capabilities.
281///
282/// Field-for-field mirror of the worker crate's `WorkerCapabilitiesAnnouncement`
283/// (the same cross-crate contract the dispatch/response pairs pin). The worker
284/// is identified by its CONNECTION (the publish's pid resolves the registered
285/// worker), never by wire-supplied identity, so an announcement can only ever
286/// apply to the worker that sent it.
287#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
288pub struct WorkerCapabilitiesAnnouncement {
289    /// The neutral intervention primitives the worker's harness supports.
290    pub capabilities: aion_core::InterventionCapabilities,
291    /// How many activities the worker runs AT ONCE, as CONFIGURED on the worker
292    /// โ€” the liminal counterpart of the gRPC `RegisterWorker.max_concurrency`
293    /// field.
294    ///
295    /// It rides this announcement rather than the registration because
296    /// `liminal::protocol::WorkerRegistration` is a published wire type with no
297    /// capacity field. Until it arrives the server holds the worker at capacity
298    /// UNKNOWN and does not dispatch to it, so this is a required field of the
299    /// announcement and not an optional embellishment: a worker that omits it
300    /// never becomes selectable.
301    pub max_concurrency: u32,
302}
303
304/// Tolerant decode of a [`WorkerCapabilitiesAnnouncement`], used ONLY by the tap.
305///
306/// Identical to the announcement except that `max_concurrency` is optional here.
307/// The wire type keeps it required โ€” a worker that omits it is not sending a
308/// valid announcement โ€” but the SERVER must be able to tell "this payload is not
309/// an announcement at all" from "this is an announcement from a worker that
310/// predates the capacity field", because the two have completely different
311/// operator consequences and only one of them is a parse problem.
312///
313/// Absent means the worker stays at capacity unknown and is never selected,
314/// which the tap says out loud at ERROR with that consequence named.
315#[derive(Debug, Deserialize)]
316struct AnnouncementProbe {
317    /// The neutral intervention primitives the worker's harness supports.
318    capabilities: aion_core::InterventionCapabilities,
319    /// The worker's advertised capacity, absent on a pre-capacity build.
320    max_concurrency: Option<u32>,
321}
322
323/// Wire liveness beat for one in-flight dispatch (the liminal mirror of the
324/// gRPC `Heartbeat` frame, liveness-only โ€” progress payloads are not carried).
325///
326/// Field-for-field mirror of the worker crate's `WorkerLivenessBeat` (same
327/// serde field names + `aion-core` id types), the same cross-crate contract the
328/// dispatch/response pairs pin. The worker is identified by its CONNECTION (the
329/// publish's pid resolves the registered worker), never by wire-supplied
330/// identity, so a beat can only ever refresh tasks of the worker that sent it.
331#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
332pub struct WorkerLivenessBeat {
333    /// Workflow owning the in-flight activity being kept alive.
334    pub workflow_id: WorkflowId,
335    /// Pinned ordinal of the in-flight activity being kept alive.
336    pub ordinal: u64,
337}
338
339/// The single reserved character that separates channel segments. Because
340/// `namespace`/`task_queue` are free-form, any occurrence of this byte INSIDE a
341/// segment must be escaped so it cannot be mistaken for the segment boundary.
342const SEGMENT_SEPARATOR: char = '.';
343
344/// The escape character used by [`encode_segment`]. It must itself be escaped so
345/// the encoding stays injective (otherwise `%2E` as a literal field value would
346/// collide with an encoded `.`).
347const SEGMENT_ESCAPE: char = '%';
348
349/// Percent-encodes the two reserved characters (`.` and `%`) inside one channel
350/// segment so distinct segment values can never collide across the join.
351///
352/// This is a minimal, deterministic, per-segment escape: a literal `.` becomes
353/// `%2E` and a literal `%` becomes `%25`; every other byte (including the empty
354/// string) passes through unchanged. Because both the separator AND the escape
355/// char are encoded, the mapping `value -> encoded` is injective: it is exactly
356/// reversible by replacing `%2E -> .` and `%25 -> %`, so two distinct values
357/// can never encode to the same string. Dot-free, percent-free inputs (the
358/// normal case, e.g. `"remote"`, `"gpu"`) are returned byte-for-byte unchanged,
359/// so existing channels are stable.
360fn encode_segment(segment: &str) -> String {
361    // Fast path: nothing reserved, return an owned copy unchanged.
362    if !segment.contains([SEGMENT_SEPARATOR, SEGMENT_ESCAPE]) {
363        return segment.to_owned();
364    }
365    let mut encoded = String::with_capacity(segment.len());
366    for ch in segment.chars() {
367        match ch {
368            // Encode the escape char FIRST so an already-present `%` cannot be
369            // confused with one we introduce for the separator.
370            SEGMENT_ESCAPE => encoded.push_str("%25"),
371            SEGMENT_SEPARATOR => encoded.push_str("%2E"),
372            other => encoded.push(other),
373        }
374    }
375    encoded
376}
377
378/// Derives the liminal dispatch channel for a worker pool addressed
379/// `(namespace, task_queue)`, optionally pinned to a specific `node`.
380///
381/// This is the **single, total source of truth** for the channel string: every
382/// site that needs the channel a `(namespace, task_queue[, node])` pool
383/// dispatches to โ€” both this dispatcher and any future worker-pool subscription
384/// side โ€” MUST call this function so the two sides cannot drift. The format is
385/// `"aion.dispatch.{namespace}.{task_queue}"` for an unpinned dispatch and
386/// `"aion.dispatch.{namespace}.{task_queue}.{node}"` when a `node` is pinned;
387/// each `{segment}` is independently passed through [`encode_segment`].
388///
389/// # The subscriber contract (the seam this function pins, NODE-5 / 13-x)
390///
391/// The subscriber side remains the documented seam (it does not exist yet; 13-0
392/// uses liminal's in-server echo responder). The contract both sides MUST honour:
393///
394/// - An **unpinned** worker pool addressed `(namespace, task_queue)` subscribes
395///   to `dispatch_channel_name(namespace, task_queue, None)` and receives every
396///   unpinned dispatch for that pool.
397/// - A **node-pinned** dispatch (the row carries `Some(node)`) goes to
398///   `dispatch_channel_name(namespace, task_queue, Some(node))`, a DISTINCT
399///   channel. A worker running on that node which is meant to serve pinned work
400///   for the pool MUST ALSO subscribe to that node-specific channel โ€” the
401///   `None` channel alone will never deliver a node-pinned dispatch to it.
402///
403/// Because the `None` channel and any `Some(node)` channel are distinct strings,
404/// a node-pinned dispatch never reaches an unpinned-only subscriber and vice
405/// versa; node isolation is therefore enforced by the channel string itself.
406///
407/// # Injectivity (why the per-segment encode matters)
408///
409/// `namespace`, `task_queue` and `node` are all free-form (the design forbids
410/// preset categories), so a raw `format!` would be NON-injective: a `.` inside
411/// any field bleeds across the separator and pools the design declares disjoint
412/// collide onto one channel โ€” e.g. `("a.b", "c", None)` and `("a", "b.c", None)`
413/// would both yield `aion.dispatch.a.b.c`, a cross-pool leak on the very
414/// isolation dimension this routing exists to keep separate. Encoding each
415/// segment independently (the separator `.` and the escape `%` are escaped within
416/// a segment) makes the map from `(namespace, task_queue, node)` to channel
417/// string injective: distinct triples always yield distinct channels, ACROSS
418/// segment counts too. The node segment is appended only for `Some(node)`, and
419/// because no encoded segment can contain a bare separator, a 2-segment channel
420/// (unpinned) can never be confused with a 3-segment channel (pinned) โ€” e.g.
421/// `("a", "b", Some("c"))` and `("a", "b.c", None)` stay distinct, as do
422/// `("a.b", "c", None)` and `("a", "b", Some("c"))`.
423///
424/// `activity_type` is deliberately NOT part of the channel: it is *what to run*,
425/// matched by the worker after delivery (it rides inside [`DispatchRequest`]),
426/// not *which pool* โ€” see `docs/NAMESPACE-TASKQUEUE-SPLIT-DESIGN.md` ยง4.2. The
427/// function is total (defined for every input) and stable (the same
428/// `(namespace, task_queue, node)` always yields the same channel).
429#[must_use]
430pub fn dispatch_channel_name(namespace: &str, task_queue: &str, node: Option<&str>) -> String {
431    let namespace = encode_segment(namespace);
432    let task_queue = encode_segment(task_queue);
433    match node {
434        Some(node) => {
435            let node = encode_segment(node);
436            format!("aion.dispatch.{namespace}.{task_queue}.{node}")
437        }
438        None => format!("aion.dispatch.{namespace}.{task_queue}"),
439    }
440}
441
442/// Derives the liminal dispatch channel for a claimed outbox row.
443///
444/// Thin wrapper over [`dispatch_channel_name`] reading the row's durable
445/// `(namespace, task_queue)` (NSTQ-2 columns) and its optional `node` (NODE-2):
446/// when `row.node` is `Some`, the row dispatches to the node-pinned sub-channel;
447/// when `None`, it derives the byte-identical unpinned channel. Kept
448/// free-standing so the dispatch path and tests derive the row's channel
449/// identically.
450#[must_use]
451pub fn channel_for_row(row: &OutboxRow) -> String {
452    dispatch_channel_name(&row.namespace, &row.task_queue, row.node.as_deref())
453}
454
455/// Wraps a reason in the existing worker-dispatch error so a non-deliverable
456/// dispatch drives the outbox's unchanged retry/backoff/dead-letter path. The
457/// row-derived `channel` is surfaced as the `activity_type` field for operator
458/// diagnostics (the field is a free-form context string on this transport's
459/// error).
460fn dispatch_error(channel: &str, reason: String) -> ServerError {
461    ServerError::WorkerDispatch {
462        namespace: "liminal".to_owned(),
463        activity_type: channel.to_owned(),
464        reason,
465    }
466}
467
468/// Receives one worker result over liminal and re-enters it into aion.
469///
470/// Holds the installed [`OutboxDeliveryCallback`] (the same prod
471/// `ServerOutboxDeliveryCallback` the gRPC completion path uses) and maps a
472/// [`DispatchResponse`] onto it, threading `run_id` so the continue-as-new run
473/// gates apply unchanged.
474pub struct LiminalCompletionSource {
475    callback: Arc<dyn OutboxDeliveryCallback>,
476    completion_fences: CompletionFences,
477}
478
479impl std::fmt::Debug for LiminalCompletionSource {
480    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
481        f.debug_struct("LiminalCompletionSource")
482            .finish_non_exhaustive()
483    }
484}
485
486impl LiminalCompletionSource {
487    /// Build a completion source over the shared outbox delivery callback.
488    #[must_use]
489    pub fn new(callback: Arc<dyn OutboxDeliveryCallback>) -> Self {
490        Self {
491            callback,
492            completion_fences: CompletionFences::default(),
493        }
494    }
495
496    /// Share the authoritative generation registry used by dispatch.
497    #[must_use]
498    pub fn with_completion_fences(mut self, completion_fences: CompletionFences) -> Self {
499        self.completion_fences = completion_fences;
500        self
501    }
502
503    /// Re-enter one worker result into aion through the delivery callback.
504    ///
505    /// Returns the callback's `bool`: `true` when delivered to a live run,
506    /// `false` when no run is live (the expected stale-completion drop that
507    /// recovery re-arms). A success outcome routes to `deliver_completion`; a
508    /// failure outcome to `deliver_failure`.
509    ///
510    /// # Errors
511    ///
512    /// Returns [`ServerError`] when the response carries an unparseable id or
513    /// the engine rejects the delivery.
514    pub fn deliver(&self, response: &DispatchResponse) -> Result<bool, ServerError> {
515        let run_id = response.run_id.as_ref().ok_or_else(|| {
516            dispatch_error(
517                "liminal completion",
518                "activity result run id is missing; refusing run-gate bypass".to_owned(),
519            )
520        })?;
521        let activity_id = ActivityId::from_sequence_position(response.ordinal);
522        let completion_token = CompletionToken::from_wire(
523            &response.workflow_id,
524            &activity_id,
525            response.completion_token.clone(),
526        )?;
527        match self
528            .completion_fences
529            .accept(&response.workflow_id, &activity_id, &completion_token)
530        {
531            Ok(accepted) => {
532                // The consumed generation is bound and deliberately NOT
533                // restored: unlike the bridge, this path has no settlement that
534                // can fail after acceptance โ€” the callback below IS the
535                // settlement and its failure is returned straight to the
536                // caller โ€” so there is no `restore_if_absent` site on this
537                // transport for the slip to feed.
538                tracing::debug!(
539                    workflow_id = %response.workflow_id,
540                    %activity_id,
541                    attempt = accepted.attempt(),
542                    "liminal completion consumed the execution generation"
543                );
544            }
545            Err(error) => {
546                tracing::warn!(
547                    workflow_id = %response.workflow_id,
548                    %activity_id,
549                    %error,
550                    "liminal completion fence rejected a late or stale reply"
551                );
552                return Err(error);
553            }
554        }
555        match &response.outcome {
556            Ok(result) => self.callback.deliver_completion(
557                &response.workflow_id,
558                &activity_id,
559                Some(run_id),
560                result.clone(),
561            ),
562            Err(reason) => self.callback.deliver_failure(
563                &response.workflow_id,
564                &activity_id,
565                Some(run_id),
566                reason.clone(),
567            ),
568        }
569    }
570}
571
572/// Rebuilds the activity input payload from the wire request.
573///
574/// The aion side tags activity input as JSON; the wire carries the raw bytes, so
575/// a worker (or the test responder standing in for one) reconstructs the typed
576/// [`Payload`] with the JSON content type.
577#[must_use]
578pub fn payload_from_request(request: &DispatchRequest) -> Payload {
579    Payload::new(ContentType::Json, request.input.clone())
580}
581
582/// Delivery handle for a liminal-connected worker held in the worker registry.
583///
584/// A worker that connects over liminal is a first-class registry member selected
585/// the SAME way as a gRPC worker (`select_worker` on `(namespace, task_queue,
586/// node)`); this is the delivery leg the registry holds for it. It pairs the
587/// [`ConnectionSupervisor`] that owns the worker's connection with that
588/// connection's beamr `pid`, so [`Self::push_dispatch`] can push a
589/// [`DispatchRequest`] out on the worker's existing socket (the LSUB-0
590/// server-push primitive) and [`receive_bridge_reply`] can block for the
591/// correlated [`DispatchResponse`].
592#[derive(Clone)]
593pub struct LiminalWorkerDelivery {
594    supervisor: ConnectionSupervisor,
595    pid: u64,
596}
597
598impl std::fmt::Debug for LiminalWorkerDelivery {
599    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
600        f.debug_struct("LiminalWorkerDelivery")
601            .field("pid", &self.pid)
602            .finish_non_exhaustive()
603    }
604}
605
606impl LiminalWorkerDelivery {
607    /// Build a delivery handle for the worker reachable on connection `pid`
608    /// through `supervisor`.
609    #[must_use]
610    pub const fn new(supervisor: ConnectionSupervisor, pid: u64) -> Self {
611        Self { supervisor, pid }
612    }
613
614    /// The connection pid this worker is addressed on.
615    #[must_use]
616    pub const fn pid(&self) -> u64 {
617        self.pid
618    }
619
620    /// Whether the supervisor still owns this worker's connection.
621    ///
622    /// The liminal answer to "is the push leg open", which the expiry sweep
623    /// needs as an explicit fact rather than inferring it from silence. It asks
624    /// the supervisor's own connection table โ€” the same table the push goes
625    /// through โ€” so a connection that has closed is structurally absent rather
626    /// than merely quiet. It says nothing about whether the worker ANSWERS on
627    /// that connection: that is the liveness probe's question, and the two are
628    /// deliberately separate facts.
629    #[must_use]
630    pub fn is_connected(&self) -> bool {
631        self.supervisor.is_tracked(self.pid)
632    }
633
634    /// Push one dispatch and wait for its reply without an activity-duration
635    /// bound, abandoning only when `keep_waiting` becomes false at a poll
636    /// boundary โ€” [`Self::push_dispatch`] then [`receive_bridge_reply`] in one
637    /// call.
638    ///
639    /// The production arms (the outbox delivery and the engine-seam bridge) do
640    /// NOT call this: they call the two halves themselves because the lease
641    /// accept hook ([`super::task_delivery::DeliveryAccepted`]) must run between
642    /// the push being acknowledged and the reply wait โ€” on liminal the reply IS
643    /// the completion, and a lease recorded after it would be a lease recorded
644    /// after the fact. This composition is for callers outside the crate that
645    /// stand in for a dispatcher (the e2e harnesses), where the receive half is
646    /// not reachable on its own.
647    ///
648    /// # Errors
649    ///
650    /// Returns the same typed push, receive, and decode failures as the shared
651    /// bridge wait.
652    pub fn dispatch_held(
653        &self,
654        request: &DispatchRequest,
655        keep_waiting: impl Fn() -> bool,
656    ) -> Result<Option<DispatchResponse>, ServerError> {
657        let awaiter = self.push_dispatch(request)?;
658        receive_bridge_reply(&awaiter, keep_waiting)
659    }
660
661    /// Serialize and push one dispatch out on the worker's connection, returning
662    /// the awaiter for its correlated reply โ€” the shared push half of both
663    /// unbounded waits: the outbox arm (`LiminalTaskDelivery`) and the
664    /// engine-seam bridge dispatcher both push here and wait with
665    /// [`receive_bridge_reply`], matching the gRPC bridge contract. One frame
666    /// format, one push primitive, one held wait policy. The two halves are
667    /// separate calls so the attempt's lease can be recorded between them,
668    /// at the instant the push is acknowledged (WA-010 R3).
669    ///
670    /// DELIBERATELY NO REPLY DEADLINE, unlike the liveness and intervention
671    /// pushes. A dispatch's reply IS the activity's completion, so its lifetime
672    /// is the activity's โ€” `remote_gates.awl` declares legs at `timeout 45m`.
673    /// [`receive_bridge_reply`] re-arms on every poll timeout and waits
674    /// unbounded by contract, so this push NEVER abandons its slot and cannot
675    /// leak one: the slot is held for exactly as long as work is outstanding
676    /// and resolves when the reply lands. Attaching a deadline here would kill
677    /// long activities, and would be a regression, not a hardening. The leak
678    /// fixed on the other two sites came from ABANDONING a no-deadline slot,
679    /// which this site never does.
680    ///
681    /// # Errors
682    ///
683    /// Returns [`ServerError::WorkerDispatchUnservable`] when liminal proves the
684    /// frame exceeds the connection's whole outbound buffer (never retried),
685    /// [`ServerError::WorkerBusy`] when liminal's typed connection cap refuses
686    /// admission, [`ServerError::WorkerConnectionLost`] for every other
687    /// push-enqueue failure, and [`ServerError::WorkerDispatch`] when the request
688    /// cannot be serialized.
689    pub(crate) fn push_dispatch(
690        &self,
691        request: &DispatchRequest,
692    ) -> Result<PushReplyAwaiter, ServerError> {
693        let payload = serde_json::to_vec(request).map_err(|error| {
694            dispatch_error("liminal-push", format!("request serialize failed: {error}"))
695        })?;
696        self.supervisor
697            .push_to_connection(self.pid, payload)
698            .map_err(|error| classify_push_error(&error))
699    }
700
701    /// Push already-encoded `payload` out on the worker's connection with an
702    /// explicit reply `deadline`, returning the awaiter for its correlated
703    /// reply.
704    ///
705    /// The raw push primitive behind the typed pushes above, used by the
706    /// connection liveness probe
707    /// ([`LivenessProbe`](super::liminal_liveness::LivenessProbe)) so the
708    /// dead-man switch rides the SAME server-push leg a dispatch does โ€” a ping
709    /// that reaches the worker proves the exact path a dispatch would take, not
710    /// a parallel one that could be healthy while the real one is not.
711    ///
712    /// The deadline is LOAD-BEARING and is why this is not
713    /// `push_to_connection`. Riding the dispatch leg means sharing its bounded
714    /// ยง5 `max_pending_pushes_per_connection` admission, and a no-deadline slot
715    /// is reclaimed only by a consumed reply or a connection close โ€” never by
716    /// the caller giving up. A probe that abandons an unanswered ping every
717    /// cadence therefore LEAKS one slot per round until the cap is exhausted,
718    /// at which point no dispatch, intervention or ping can be pushed on that
719    /// connection again. Measured live on run `dfd2117c`: 32 abandoned pings,
720    /// then permanent refusal, โ‰ˆ240 s from first blocked ping. The probe built
721    /// to prove the dispatch path works is what destroyed it. With a deadline,
722    /// expiry removes the slot and RELEASES its cap admission.
723    ///
724    /// # Errors
725    ///
726    /// Returns [`ServerError::WorkerDispatchUnservable`] when liminal proves the
727    /// frame exceeds the connection's whole outbound buffer,
728    /// [`ServerError::WorkerBusy`] when liminal's typed connection cap refuses
729    /// admission, and [`ServerError::WorkerConnectionLost`] for every other
730    /// push-enqueue failure.
731    pub fn push_payload_with_deadline(
732        &self,
733        payload: Vec<u8>,
734        deadline: Duration,
735    ) -> Result<PushReplyAwaiter, ServerError> {
736        self.supervisor
737            .push_to_connection_with_deadline(self.pid, payload, deadline)
738            .map_err(|error| classify_push_error(&error))
739    }
740
741    /// Push one neutral intervention command out on the worker's connection and
742    /// block for its correlated ack reply (NOI-6, ยง6.2).
743    ///
744    /// Uses the same connection as activity dispatch but carries an
745    /// [`InterventionRequest`] and decodes an [`InterventionReply`], so it rides the SAME server-push
746    /// channel as an activity dispatch. The push is a blocking, thread-based liminal
747    /// call; the async router runs it off the runtime.
748    ///
749    /// # Errors
750    ///
751    /// Returns [`ServerError::WorkerConnectionLost`] when the worker connection was
752    /// gone at push time or closed before replying (so the router surfaces the
753    /// too-late no-op); returns [`ServerError::WorkerDispatch`] when the request
754    /// cannot be serialized, the reply times out, or the reply cannot be decoded.
755    pub fn push_intervention(
756        &self,
757        request: &InterventionRequest,
758    ) -> Result<InterventionReply, ServerError> {
759        let payload = serde_json::to_vec(request).map_err(|error| {
760            dispatch_error(
761                "liminal-push",
762                format!("intervention serialize failed: {error}"),
763            )
764        })?;
765        // Deadline-bounded, and it must be: this wait ABANDONS on timeout (the
766        // `receive` error below is terminal, not a re-arm), so a no-deadline
767        // slot would be held until the connection closed and would count
768        // against the connection's ยง5 pending-push cap forever. The deadline
769        // matches the wait, so an intervention that goes unanswered releases
770        // its admission instead of leaking it. Contrast `push_dispatch`, whose
771        // wait re-arms and is unbounded by contract, and which therefore
772        // correctly takes no deadline.
773        let awaiter = self
774            .supervisor
775            .push_to_connection_with_deadline(self.pid, payload, PUSH_REPLY_TIMEOUT)
776            .map_err(|error| {
777                ServerError::worker_connection_lost(
778                    "liminal-push",
779                    format!("push intervention to worker failed: {error}"),
780                )
781            })?;
782        let reply = awaiter.receive(PUSH_REPLY_TIMEOUT).map_err(|error| {
783            if is_connection_closed_reply_error(&error) {
784                ServerError::worker_connection_lost(
785                    "liminal-push",
786                    format!("worker connection closed before intervention ack: {error}"),
787                )
788            } else {
789                dispatch_error("liminal-push", format!("intervention ack failed: {error}"))
790            }
791        })?;
792        serde_json::from_slice(&reply).map_err(|error| {
793            dispatch_error(
794                "liminal-push",
795                format!("intervention ack decode failed: {error}"),
796            )
797        })
798    }
799}
800
801/// Decodes one correlated reply payload as a [`DispatchResponse`].
802///
803/// Shared by the outbox arm and the bridge โ€” both wait through
804/// [`receive_bridge_reply`] โ€” so the two paths can never diverge on the wire's
805/// reply shape.
806fn decode_dispatch_response(reply: &[u8]) -> Result<DispatchResponse, ServerError> {
807    serde_json::from_slice(reply).map_err(|error| {
808        dispatch_error(
809            "liminal-push",
810            format!("worker reply decode failed: {error}"),
811        )
812    })
813}
814
815/// Blocks for the correlated reply to an engine-seam BRIDGE dispatch push, with
816/// the bridge's UNBOUNDED wait contract: the engine imposes no activity timeout
817/// of its own, so an elapsed [`BRIDGE_REPLY_POLL`] merely re-arms the wait โ€” the
818/// exact liminal mirror of the gRPC bridge's unbounded `recv`, which is released
819/// only by a completion or by stream teardown. The wait terminates on exactly:
820///
821/// - **the reply** โ€” decoded as the worker's [`DispatchResponse`] and returned
822///   as `Ok(Some(response))`. A reply already buffered when a poll fires always
823///   wins over abandonment: the awaiter is drained before `keep_waiting` runs;
824/// - **abandonment** โ€” `keep_waiting()` returned `false` at a poll boundary
825///   (the bridge passes "is this dispatch still tracked in-flight?"): the
826///   dispatch was resolved by another path (expiry sweep, shutdown drain, or a
827///   cleanup), so the wait returns `Ok(None)` and the router thread exits
828///   instead of parking on the connection indefinitely;
829/// - **an oversize frame** โ€” liminal's connection process proved the encoded
830///   dispatch larger than the connection's whole outbound buffer and failed the
831///   reply slot BY NAME. The refusal arrives HERE, on the awaiter, not on the
832///   push call: `push_to_connection` only reserves the slot, the enqueue that
833///   measures the frame runs afterwards in the connection process. It is
834///   surfaced as [`ServerError::WorkerDispatchUnservable`], the class both
835///   dispatch arms settle terminally on first observation, so the outbox
836///   dead-letters the row and the bridge reports `terminal:` instead of
837///   retrying a certainty;
838/// - **worker loss** โ€” the connection closed before replying: the awaiter wakes
839///   PROMPTLY with liminal's typed Disconnected error, surfaced as
840///   [`ServerError::WorkerConnectionLost`] so the bridge reports the same
841///   TRANSPORT-loss class the gRPC teardown sweep does;
842/// - **an unrecognized receive fault or a decode failure** โ€” surfaced as
843///   [`ServerError::WorkerDispatch`].
844///
845/// Runs on a dedicated bridge reply thread, never on an async runtime worker.
846///
847/// # Errors
848///
849/// Returns [`ServerError::WorkerDispatchUnservable`] when liminal proves the
850/// frame exceeds the connection's whole outbound buffer,
851/// [`ServerError::WorkerConnectionLost`] on worker loss and
852/// [`ServerError::WorkerDispatch`] on a receive fault or reply decode failure.
853pub(crate) fn receive_bridge_reply(
854    awaiter: &PushReplyAwaiter,
855    keep_waiting: impl Fn() -> bool,
856) -> Result<Option<DispatchResponse>, ServerError> {
857    loop {
858        match awaiter.receive(BRIDGE_REPLY_POLL) {
859            Ok(reply) => return decode_dispatch_response(&reply).map(Some),
860            // A bare poll timeout re-arms the wait while the dispatch is still
861            // outstanding (the wait is unbounded by contract; see the bridge
862            // module docs), and ends it cleanly once the dispatch was resolved
863            // elsewhere โ€” the poll cadence bounds the router thread's lifetime
864            // to one poll past that resolution.
865            Err(LiminalServerError::PushReplyTimeout { .. }) => {
866                if !keep_waiting() {
867                    return Ok(None);
868                }
869            }
870            Err(error) => return Err(classify_reply_error(&error)),
871        }
872    }
873}
874
875/// Classify a typed liminal push-REPLY failure without relying on error text โ€”
876/// the receive-side twin of [`classify_push_error`].
877///
878/// The oversize-frame refusal is checked FIRST and by name, exactly as on the
879/// push side, because this is the arm it actually arrives on: liminal settles
880/// that slot from the connection process after the push call has returned. A
881/// generic wrap here read as the retryable dispatch class, and that is how a
882/// frame that could never fit was re-pushed for every attempt in the budget
883/// while the operator's refusal said "retryable".
884fn classify_reply_error(error: &LiminalServerError) -> ServerError {
885    if let Some(unservable) = unservable_frame_refusal(error) {
886        return unservable;
887    }
888    if is_connection_closed_reply_error(error) {
889        return ServerError::worker_connection_lost(
890            "liminal-push",
891            format!("worker connection closed before reply: {error}"),
892        );
893    }
894    dispatch_error("liminal-push", format!("worker reply failed: {error}"))
895}
896
897/// Returns true when a liminal push-reply error is the *Disconnected* case (the
898/// worker's connection closed before it replied), as opposed to a genuine reply
899/// timeout (the worker is alive but slow).
900///
901/// Liminal returns these as distinct TYPED variants โ€”
902/// `ServerError::PushReplyDisconnected` vs `PushReplyTimeout` (see `liminal-server`
903/// `supervisor.rs` `PushReplyAwaiter::receive`) โ€” so this is a type match, not a
904/// message-text match: a worker that DIED (connection-lost, fast failover) is told
905/// apart from one that is merely SLOW (genuine timeout, normal backoff) by variant.
906fn is_connection_closed_reply_error(error: &LiminalServerError) -> bool {
907    matches!(error, LiminalServerError::PushReplyDisconnected { .. })
908}
909
910/// The one refusal both halves of a push must recognise, by variant: liminal
911/// proved the encoded frame larger than the connection's whole outbound buffer.
912///
913/// A frame that cannot fit the whole buffer can never be carried, so it must
914/// never fall into a lost-connection or generic dispatch arm โ€” both read as
915/// "fail over and retry", and retrying a certainty is exactly what wrote 846
916/// leases for one activity. The detail names the two numbers and the key an
917/// operator would change. `None` for every other error, which each caller
918/// then classifies for its own side of the push.
919fn unservable_frame_refusal(error: &LiminalServerError) -> Option<ServerError> {
920    let LiminalServerError::PushFrameExceedsOutboundCapacity {
921        needed, capacity, ..
922    } = error
923    else {
924        return None;
925    };
926    Some(ServerError::worker_dispatch_unservable(
927        "liminal-push",
928        format!(
929            "the dispatch frame is {needed} bytes and this worker connection's whole outbound \
930             buffer is {capacity} bytes (`outbox.{OUTBOUND_BOUND_KEY}`); no retry can carry \
931             it: {error}"
932        ),
933    ))
934}
935
936/// Classify a typed liminal push refusal without relying on error text.
937fn classify_push_error(error: &LiminalServerError) -> ServerError {
938    if let Some(unservable) = unservable_frame_refusal(error) {
939        return unservable;
940    }
941    if is_connection_cap_error(error) {
942        ServerError::worker_busy(
943            "liminal-push",
944            format!("worker connection push cap reached: {error}"),
945        )
946    } else {
947        ServerError::worker_connection_lost(
948            "liminal-push",
949            format!("push to worker failed: {error}"),
950        )
951    }
952}
953
954/// The `[outbox]` key that sets each worker connection's outbound bound. Named
955/// in the unservable refusal so the operator reading it knows what to change;
956/// spelled once here and once on the config field, and a test pins them equal.
957pub(crate) const OUTBOUND_BOUND_KEY: &str = "liminal_max_connection_outbound_bytes";
958
959/// Liminal's typed admission refusal when a connection has too many held pushes.
960fn is_connection_cap_error(error: &LiminalServerError) -> bool {
961    matches!(error, LiminalServerError::ConnectionCapReached { .. })
962}
963
964/// RAII guard that releases an [`AttemptOwnerIndex`](super::intervention::AttemptOwnerIndex)
965/// binding when the dispatch resolves โ€” on the reply, an error, or a panic โ€” so
966/// the back-index tracks exactly the attempts currently in flight (NOI-6).
967///
968/// Shared by both liminal dispatch arms: the outbox row wait holds it across
969/// its blocking `dispatch` call, and the engine-seam bridge hands it to the
970/// dispatch's reply-router thread, which drops it on every exit path.
971pub(crate) struct AttemptOwnerGuard {
972    owners: super::intervention::AttemptOwnerIndex,
973    key: super::intervention::AttemptKey,
974}
975
976impl AttemptOwnerGuard {
977    /// Bind `key` to `worker` in `owners` and return the guard that releases
978    /// the binding on drop.
979    pub(crate) fn bind(
980        owners: super::intervention::AttemptOwnerIndex,
981        key: super::intervention::AttemptKey,
982        worker: super::registry::WorkerId,
983    ) -> Self {
984        owners.bind(key.clone(), worker);
985        Self { owners, key }
986    }
987}
988
989impl Drop for AttemptOwnerGuard {
990    fn drop(&mut self) {
991        self.owners.release(&self.key);
992    }
993}
994
995/// Normalize a wire `node` (`Option<String>`) onto the registry's optional
996/// locality affinity, applying the SAME none-convention the gRPC registration
997/// path uses (`registry::optional_node`): an empty string carries no node, so it
998/// collapses to `None`; any non-empty value is the worker's advertised node.
999///
1000/// The wire already models `node` as `Option<String>`, but a worker that joins
1001/// `Some("")` (the empty-string node) must not register a distinct empty-node
1002/// affinity that no pinned dispatch could ever match โ€” it is semantically
1003/// unpinned, exactly as the gRPC proto3 empty default is. Folding it to `None`
1004/// here keeps the two registration paths byte-for-byte equivalent.
1005fn normalize_wire_node(node: Option<&str>) -> Option<String> {
1006    node.filter(|value| !value.is_empty())
1007        .map(ToOwned::to_owned)
1008}
1009
1010/// Connection-keyed [`ConnectionNotifier`] that turns liminal's in-band worker
1011/// registration into a first-class [`ConnectedWorkerRegistry`] membership.
1012///
1013/// This is the SERVER half of LSUB-L2: when a worker connects with a
1014/// [`WireWorkerRegistration`] (the SDK's `connect_with_registration`), liminal's
1015/// connection process invokes [`on_worker_registered`](Self::on_worker_registered)
1016/// with the connection's beamr `pid` and the worker's declared
1017/// `(namespaces, task_queue, node, activity_types)`. The notifier builds a
1018/// [`WorkerDelivery::Liminal`] over the connection and inserts it into the
1019/// registry โ€” the SAME registry entry, selected the SAME way, as a gRPC worker โ€”
1020/// retiring the LSUB-1 out-of-band `active_connection_pids()` + hard-coded
1021/// registration hack.
1022///
1023/// # Lifetime of the registration guard
1024///
1025/// [`ConnectedWorkerRegistry::register_delivery`] returns a
1026/// [`WorkerRegistration`] guard whose drop deregisters the worker. The notifier
1027/// OWNS that guard keyed by `pid` (`Mutex<HashMap<u64, WorkerRegistration>>`), so
1028/// the registration lives exactly as long as the connection: it is inserted on
1029/// register and removed (dropped) on
1030/// [`on_worker_unregistered`](Self::on_worker_unregistered), which liminal fires
1031/// on connection close.
1032///
1033/// # Construction-order cycle (notifier <-> supervisor)
1034///
1035/// [`LiminalWorkerDelivery`] needs a [`ConnectionSupervisor`] handle to push to
1036/// the worker's connection, but the supervisor is itself constructed WITH this
1037/// notifier ([`ConnectionSupervisor::with_services_and_notifier`]) โ€” a cycle. The
1038/// notifier therefore holds the supervisor behind a [`OnceLock`], populated
1039/// IMMEDIATELY after the supervisor is built via [`Self::bind_supervisor`]. The
1040/// `OnceLock` is never read before it is set in correct wiring (a worker can only
1041/// register after the listener โ€” built after the supervisor and after
1042/// `bind_supervisor` โ€” accepts its connection); if it somehow were, registration
1043/// is REJECTED with a typed error rather than panicking, so there is no
1044/// production `unwrap`/`expect` and no second always-`None` code path.
1045pub struct LiminalConnectionNotifier {
1046    registry: ConnectedWorkerRegistry,
1047    contract_catalog: Option<Arc<aion::Engine>>,
1048    supervisor: OnceLock<ConnectionSupervisor>,
1049    guards: Mutex<HashMap<u64, WorkerRegistration>>,
1050    /// The neutral intervention primitives a liminal-connected agent worker
1051    /// advertises (NOI-6, item 4). The liminal `WorkerRegistration` wire has a fixed
1052    /// shape that cannot carry this, so it is configured on the notifier at the
1053    /// composition root from the harness's advertised `AgentSession::capabilities()`
1054    /// and recorded on every registered worker's handle, where the intervention
1055    /// router gates on it. Default empty = observability-only (a plain activity
1056    /// worker), so the router offers no controls for it.
1057    intervention_capabilities: aion_core::InterventionCapabilities,
1058    /// The transcript sequencer a worker's observability publishes drain into
1059    /// (NOI-5b), plus the Tokio [`Handle`](tokio::runtime::Handle) to bridge the
1060    /// synchronous connection-process callback onto the async publish. `None` (the
1061    /// default, and every non-agent boot) makes the observability tap a no-op, so a
1062    /// worker publish to the reserved channel is simply ignored by the notifier.
1063    transcript: Option<TranscriptTap>,
1064    /// The SAME per-task liveness tracker the bridge dispatcher tracks into and
1065    /// the #176 expiry sweeper expires from. A worker's automatic
1066    /// [`WorkerLivenessBeat`] publishes on [`WORKER_LIVENESS_CHANNEL`] refresh
1067    /// their task stamps here, keeping the sweeper honest for liminal-delivered
1068    /// dispatches. `None` (a wiring that never bridges dispatches, e.g. isolated
1069    /// tests) consumes and drops the beats.
1070    heartbeat_tracker: Option<super::heartbeat::HeartbeatTracker>,
1071    /// The namespace admission every registration passes through, bound by
1072    /// the commissioning path. `None` FAILS CLOSED: a notifier nobody bound an
1073    /// admission to refuses every registration by name rather than admitting
1074    /// unguarded, which is exactly the defect this field closes.
1075    admission: Option<RegistrationAdmission>,
1076}
1077
1078/// The observability-drain leg of the notifier: a bounded, ORDERED queue into
1079/// the one drain task that publishes transcript events sequentially.
1080///
1081/// One consumer is load-bearing, not an implementation detail: a spawned task
1082/// per event (the pre-2026-07-23 shape) made every in-flight frame a
1083/// concurrent writer racing the same stream head, turning the sequencer's
1084/// optimistic-append loop into an O(Nยฒ) conflict stampede under load โ€” and
1085/// destroyed `worker_seq` order on the durable transcript. Sequential
1086/// draining preserves arrival order and leaves the conflict-retry loop to
1087/// handle only genuine cross-process races (failover adoption).
1088#[derive(Clone)]
1089struct TranscriptTap {
1090    queue: tokio::sync::mpsc::Sender<aion_core::ActivityEvent>,
1091}
1092
1093/// Bound on the transcript drain queue: events a slow durable append cannot
1094/// keep up with are dropped (with a warning) rather than buffered without
1095/// limit. Live streaming is best-effort; the bound protects server memory.
1096const TRANSCRIPT_QUEUE_CAPACITY: usize = 4096;
1097
1098impl std::fmt::Debug for LiminalConnectionNotifier {
1099    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1100        f.debug_struct("LiminalConnectionNotifier")
1101            .field("supervisor_bound", &self.supervisor.get().is_some())
1102            .finish_non_exhaustive()
1103    }
1104}
1105
1106impl LiminalConnectionNotifier {
1107    /// Build a notifier that registers connecting workers into `registry`.
1108    ///
1109    /// The supervisor handle is bound separately via [`Self::bind_supervisor`]
1110    /// immediately after the supervisor is constructed, resolving the
1111    /// notifier <-> supervisor construction cycle (see the type docs).
1112    #[must_use]
1113    pub fn new(registry: ConnectedWorkerRegistry) -> Self {
1114        Self {
1115            registry,
1116            contract_catalog: None,
1117            supervisor: OnceLock::new(),
1118            guards: Mutex::new(HashMap::new()),
1119            intervention_capabilities: aion_core::InterventionCapabilities::none(),
1120            transcript: None,
1121            heartbeat_tracker: None,
1122            admission: None,
1123        }
1124    }
1125
1126    /// Bind the namespace admission a liminal registration is judged by โ€”
1127    /// the same guard, mint-or-gate and placement admission the gRPC path
1128    /// applies โ€” and the Tokio runtime `handle` the synchronous
1129    /// connection-process callback bridges onto to run it.
1130    ///
1131    /// The handle is named, never captured from ambient context: the e2e
1132    /// harnesses build this notifier in a sync `start()` BEFORE they build
1133    /// their runtime, and a capture there was `None`, refusing every
1134    /// registration on the very tests meant to exercise it while the
1135    /// production path (built inside `run_server`) happened to work. A
1136    /// listener commissioned outside any runtime has no handle to give and
1137    /// refuses to be commissioned, at the call site, by name.
1138    ///
1139    /// `auth_enabled` mirrors `auth.enabled`: the liminal protocol's
1140    /// registration frame carries an `identity` string and no credential, so
1141    /// on a server that authenticates callers a liminal registration cannot
1142    /// be scoped and is refused by name; on an auth-off server it is admitted
1143    /// as the operator identity, exactly as a gRPC registration with no
1144    /// metadata is.
1145    #[must_use]
1146    pub fn with_admission(
1147        mut self,
1148        guard: NamespaceGuard,
1149        auth_enabled: bool,
1150        handle: tokio::runtime::Handle,
1151    ) -> Self {
1152        self.admission = Some(RegistrationAdmission {
1153            guard,
1154            auth_enabled,
1155            handle,
1156        });
1157        self
1158    }
1159
1160    /// Install the live package catalog used to refuse incompatible workers
1161    /// before liminal acknowledges or publishes their registration.
1162    #[must_use]
1163    pub fn with_contract_catalog(mut self, engine: Arc<aion::Engine>) -> Self {
1164        self.contract_catalog = Some(engine);
1165        self
1166    }
1167
1168    /// Install the shared per-task liveness tracker so a worker's automatic
1169    /// [`WorkerLivenessBeat`] publishes refresh the SAME in-flight entries the
1170    /// bridge dispatcher tracks and the #176 sweeper expires.
1171    ///
1172    /// MUST be wired on every boot that hosts the engine-seam bridge (the
1173    /// production composition does), or the sweeper would expire healthy
1174    /// liminal workers running activities longer than the heartbeat window.
1175    /// Without it (isolated tests that never bridge-dispatch) beats are
1176    /// consumed and dropped.
1177    #[must_use]
1178    pub fn with_heartbeat_tracker(mut self, tracker: super::heartbeat::HeartbeatTracker) -> Self {
1179        self.heartbeat_tracker = Some(tracker);
1180        self
1181    }
1182
1183    /// Install the transcript sequencer a worker's observability publishes drain into
1184    /// (NOI-5b), capturing the CURRENT Tokio runtime handle to bridge the synchronous
1185    /// connection-process callback onto the async publish.
1186    ///
1187    /// MUST be called from within a Tokio runtime (the server boot path is), so the
1188    /// captured [`Handle`](tokio::runtime::Handle) can spawn the append+fan-out when a
1189    /// worker publishes a transcript event over the reserved channel. Without this
1190    /// builder the observability tap is a no-op (a plain, non-agent deployment).
1191    ///
1192    /// # Panics
1193    ///
1194    /// Panics if called outside a Tokio runtime โ€” a construction-time wiring error in
1195    /// the server boot, never a runtime condition (the boot path always builds the
1196    /// notifier inside the server runtime).
1197    #[must_use]
1198    pub fn with_transcript_publisher(
1199        mut self,
1200        publisher: crate::activity_publisher::ActivityEventPublisher,
1201    ) -> Self {
1202        let (queue, mut events) =
1203            tokio::sync::mpsc::channel::<aion_core::ActivityEvent>(TRANSCRIPT_QUEUE_CAPACITY);
1204        // The ONE drain task: transcript events publish sequentially in
1205        // arrival order (see the `TranscriptTap` docs for why one consumer is
1206        // load-bearing). It lives as long as any queue sender does.
1207        //
1208        // The drain COALESCES under the operator's flush policy: the events
1209        // waiting on this queue are committed as batches, so a chatty agent
1210        // costs one storage-tree commit per batch instead of one per event โ€”
1211        // and one commit is one whole-leaf rewrite (forensics 2026-08-17).
1212        // Arrival order is untouched; only the number of commits changes.
1213        tokio::runtime::Handle::current().spawn(async move {
1214            let dropped = publisher.drain(&mut events, "observability_tap").await;
1215            if dropped > 0 {
1216                tracing::warn!(
1217                    dropped,
1218                    operation = "observability_tap",
1219                    "observability tap: transcript events were not retained"
1220                );
1221            }
1222        });
1223        self.transcript = Some(TranscriptTap { queue });
1224        self
1225    }
1226
1227    /// Set the neutral intervention capability set every worker registering through
1228    /// this notifier advertises (NOI-6, item 4).
1229    ///
1230    /// The composition root wires this from the harness's advertised
1231    /// `AgentSession::capabilities()` so a liminal-connected agent worker's handle
1232    /// carries the primitives its harness supports, which the intervention router
1233    /// gates on. Without this builder the set is empty (observability-only), so a
1234    /// plain activity worker advertises no controls. Pure builder addition, mirroring
1235    /// the registry's capability-carrying registration faรงade.
1236    #[must_use]
1237    pub fn with_intervention_capabilities(
1238        mut self,
1239        capabilities: aion_core::InterventionCapabilities,
1240    ) -> Self {
1241        self.intervention_capabilities = capabilities;
1242        self
1243    }
1244
1245    /// Bind the connection supervisor the notifier pushes through, immediately
1246    /// after it is constructed with this notifier.
1247    ///
1248    /// Returns `true` when the supervisor was stored, `false` when it was already
1249    /// bound (a second bind is a wiring bug and is ignored, never overwriting the
1250    /// live handle). Call this exactly once, right after
1251    /// [`ConnectionSupervisor::with_services_and_notifier`].
1252    pub fn bind_supervisor(&self, supervisor: ConnectionSupervisor) -> bool {
1253        self.supervisor.set(supervisor).is_ok()
1254    }
1255
1256    /// Snapshot of every live liminal connection the dead-man switch must ping:
1257    /// its pid, the worker it registered, and the push leg to reach it.
1258    ///
1259    /// Built from the SAME guard map that owns each connection's registration,
1260    /// so a connection that has closed (its guard removed) is structurally
1261    /// absent from the snapshot and is never pinged. A poisoned guard map is
1262    /// recovered rather than skipped: silently returning no targets would turn
1263    /// the dead-man switch off exactly when the server is already unhealthy.
1264    #[must_use]
1265    pub fn liveness_targets(&self) -> Vec<super::liminal_liveness::LivenessTarget> {
1266        let Some(supervisor) = self.supervisor.get() else {
1267            return Vec::new();
1268        };
1269        let guards = match self.guards.lock() {
1270            Ok(guards) => guards,
1271            Err(poisoned) => poisoned.into_inner(),
1272        };
1273        guards
1274            .iter()
1275            .filter_map(|(pid, guard)| {
1276                guard
1277                    .worker_id()
1278                    .map(|worker_id| super::liminal_liveness::LivenessTarget {
1279                        pid: *pid,
1280                        worker_id,
1281                        delivery: LiminalWorkerDelivery::new(supervisor.clone(), *pid),
1282                    })
1283            })
1284            .collect()
1285    }
1286
1287    /// Refresh one worker's in-flight task stamp from a [`WorkerLivenessBeat`]
1288    /// published on [`WORKER_LIVENESS_CHANNEL`].
1289    ///
1290    /// The worker is resolved by the publishing CONNECTION (`pid` -> the
1291    /// registration guard this notifier owns), never by wire identity, so a
1292    /// beat can only refresh tasks assigned to the worker that sent it. An
1293    /// untracked task is benign (an outbox dispatch the tracker never held, or
1294    /// a beat racing its completion) and is dropped silently; a malformed
1295    /// payload or unregistered connection is logged and dropped โ€” a bad beat
1296    /// must never tear down the connection callback.
1297    fn record_liveness_beat(&self, pid: u64, payload: &[u8]) {
1298        let Some(tracker) = &self.heartbeat_tracker else {
1299            return;
1300        };
1301        let beat: WorkerLivenessBeat = match serde_json::from_slice(payload) {
1302            Ok(beat) => beat,
1303            Err(error) => {
1304                tracing::warn!(%error, "liveness tap: malformed WorkerLivenessBeat payload");
1305                return;
1306            }
1307        };
1308        let worker_id = match self.guards.lock() {
1309            Ok(guards) => guards.get(&pid).and_then(WorkerRegistration::worker_id),
1310            Err(poisoned) => poisoned
1311                .into_inner()
1312                .get(&pid)
1313                .and_then(WorkerRegistration::worker_id),
1314        };
1315        let Some(worker_id) = worker_id else {
1316            tracing::warn!(
1317                connection_pid = pid,
1318                "liveness tap: beat from a connection with no registered worker"
1319            );
1320            return;
1321        };
1322        let activity_id = ActivityId::from_sequence_position(beat.ordinal);
1323        if let Err(error) = tracker.record_liveness(
1324            worker_id,
1325            &beat.workflow_id,
1326            &activity_id,
1327            std::time::Instant::now(),
1328        ) {
1329            tracing::error!(
1330                %error,
1331                connection_pid = pid,
1332                "liveness tap: heartbeat tracker refresh failed"
1333            );
1334        }
1335    }
1336
1337    /// Apply one worker's [`WorkerCapabilitiesAnnouncement`] published on
1338    /// [`WORKER_CAPABILITIES_CHANNEL`] to its registered handle.
1339    ///
1340    /// The worker is resolved by the publishing CONNECTION (`pid` -> the
1341    /// registration guard this notifier owns), never by wire identity, so an
1342    /// announcement can only ever update the worker that sent it. A malformed
1343    /// payload or unregistered connection is logged and dropped โ€” a bad
1344    /// announcement must never tear down the connection callback.
1345    ///
1346    /// # The two halves are applied on different terms: ABSENT is not EMPTY
1347    ///
1348    /// `max_concurrency` is applied ALWAYS. Only the worker knows it, the
1349    /// liminal registration frame has no field to carry it, and until it lands
1350    /// the worker is held at capacity unknown and is never dispatched to.
1351    ///
1352    /// `capabilities` is applied only when the worker DECLARES one โ€” an empty
1353    /// set means "I have none to add", not "clear what you have". There are two
1354    /// writers of that one field: this tap, and the notifier's
1355    /// `with_intervention_capabilities`, which stamps a SERVER-POLICY set at
1356    /// registration that the worker has no way to know. That two-writer shape
1357    /// is pre-existing and out of scope here. What is not is the consequence of
1358    /// this announcement having become unconditional so it could carry capacity:
1359    /// treating an empty set as an instruction would let every worker without
1360    /// harness capabilities of its own silently withdraw the controls an
1361    /// operator had, immediately after connecting and with nothing logged.
1362    /// Nothing is lost by the distinction โ€” capabilities are fixed for the life
1363    /// of a connection, and a redial re-registers and re-stamps.
1364    fn record_capabilities_announcement(&self, pid: u64, payload: &[u8]) {
1365        // Decoded through a TOLERANT probe, so that "this worker sent no
1366        // capacity" is reported as the condition it is rather than as a parse
1367        // failure. `max_concurrency` is a required field of the announcement, so
1368        // an older worker that predates it produced a `serde` error and a
1369        // "malformed payload" line โ€” a message that names the wrong problem and
1370        // says nothing about the consequence, which is that this worker will
1371        // never be selected for a dispatch as long as it stays connected.
1372        let announcement: AnnouncementProbe = match serde_json::from_slice(payload) {
1373            Ok(announcement) => announcement,
1374            Err(error) => {
1375                tracing::warn!(
1376                    %error,
1377                    connection_pid = pid,
1378                    "capabilities tap: malformed WorkerCapabilitiesAnnouncement payload"
1379                );
1380                return;
1381            }
1382        };
1383        let Some(max_concurrency) = announcement.max_concurrency else {
1384            tracing::error!(
1385                connection_pid = pid,
1386                "capabilities tap: this worker announced no capacity and will never be selected \
1387                 for a dispatch. The liminal registration frame cannot carry a capacity, so the \
1388                 server holds a worker at capacity UNKNOWN until it announces one; a worker that \
1389                 never does is registered, idle and unselectable. It is an older build that \
1390                 predates the capacity announcement, or its announcement was truncated"
1391            );
1392            return;
1393        };
1394        let worker_id = match self.guards.lock() {
1395            Ok(guards) => guards.get(&pid).and_then(WorkerRegistration::worker_id),
1396            Err(poisoned) => poisoned
1397                .into_inner()
1398                .get(&pid)
1399                .and_then(WorkerRegistration::worker_id),
1400        };
1401        let Some(worker_id) = worker_id else {
1402            tracing::warn!(
1403                connection_pid = pid,
1404                "capabilities tap: announcement from a connection with no registered worker"
1405            );
1406            return;
1407        };
1408        // CAPACITY FIRST. It is the field that decides whether this worker is
1409        // dispatchable at all โ€” it registered with capacity unknown, which
1410        // selection reads as full โ€” so a failure here must be reported at
1411        // ERROR and must not be masked by the capability update that follows.
1412        match self
1413            .registry
1414            .set_advertised_capacity(worker_id, max_concurrency)
1415        {
1416            Ok(true) => {}
1417            Ok(false) => tracing::warn!(
1418                connection_pid = pid,
1419                worker_id = ?worker_id,
1420                "capabilities tap: capacity announcement raced the worker's deregistration"
1421            ),
1422            Err(error) => tracing::error!(
1423                %error,
1424                connection_pid = pid,
1425                worker_id = ?worker_id,
1426                announced_max_concurrency = max_concurrency,
1427                "capabilities tap: worker announced a capacity the registry refused; it stays \
1428                 unknown-capacity and no dispatch will be selected for it"
1429            ),
1430        }
1431        // AN EMPTY CAPABILITY SET DOES NOT CLEAR THE REGISTRATION STAMP.
1432        //
1433        // This announcement used to be sent only by a worker that had
1434        // capabilities to declare; it is unconditional now, because it also
1435        // carries the capacity without which the worker is never dispatched to.
1436        // That made the capability half destructive: the notifier stamps a set
1437        // at REGISTRATION (its `with_intervention_capabilities`, the server's
1438        // own policy for liminal workers, which the worker has no way to know),
1439        // and a worker with no harness capabilities of its own would then
1440        // immediately overwrite it with the empty set โ€” silently withdrawing
1441        // every intervention control an operator had.
1442        //
1443        // So the two halves of the announcement are applied on different terms,
1444        // and deliberately: the capacity is always applied because only the
1445        // worker knows it, and the capability set is applied only when the
1446        // worker actually declares one. Nothing is lost by that โ€” capabilities
1447        // are fixed for the life of a connection, and a redial re-registers and
1448        // re-stamps โ€” and it keeps the exact behaviour that held when silence
1449        // was how a worker said "I have none".
1450        if announcement.capabilities.supported.is_empty() {
1451            return;
1452        }
1453        match self
1454            .registry
1455            .set_intervention_capabilities(worker_id, &announcement.capabilities)
1456        {
1457            Ok(true) => {}
1458            Ok(false) => tracing::warn!(
1459                connection_pid = pid,
1460                worker_id = ?worker_id,
1461                "capabilities tap: announcement raced the worker's deregistration"
1462            ),
1463            Err(error) => tracing::error!(
1464                %error,
1465                connection_pid = pid,
1466                "capabilities tap: registry capability update failed"
1467            ),
1468        }
1469    }
1470
1471    fn validate_registration_contract(
1472        &self,
1473        registration: &WireWorkerRegistration,
1474    ) -> Result<(), LiminalServerError> {
1475        let Some(engine) = &self.contract_catalog else {
1476            return Ok(());
1477        };
1478        let advertised =
1479            registration
1480                .activities
1481                .iter()
1482                .map(|activity| {
1483                    let input_schema =
1484                        serde_json::from_str(&activity.input_schema_json).map_err(|error| {
1485                            LiminalServerError::ListenerAccept {
1486                                message: format!(
1487                                    "liminal worker activity `{}` input schema is invalid: {error}",
1488                                    activity.name
1489                                ),
1490                            }
1491                        })?;
1492                    let output_schema = serde_json::from_str(&activity.output_schema_json)
1493                        .map_err(|error| LiminalServerError::ListenerAccept {
1494                            message: format!(
1495                                "liminal worker activity `{}` output schema is invalid: {error}",
1496                                activity.name
1497                            ),
1498                        })?;
1499                    Ok(aion_package::ActivityDescriptor {
1500                        name: activity.name.clone(),
1501                        input_schema,
1502                        output_schema,
1503                    })
1504                })
1505                .collect::<Result<Vec<_>, LiminalServerError>>()?;
1506        // Both advertised forms travel into the gate together: the NAME set the
1507        // dispatcher selects on and the CONTRACT set admission compares. A
1508        // refusal computed from one while the record printed the other is what
1509        // produced the 2026-07-30 self-contradicting log.
1510        let activity_types = registration
1511            .activity_types
1512            .iter()
1513            .cloned()
1514            .collect::<std::collections::BTreeSet<_>>();
1515        super::contracts::validate_worker_contracts(
1516            engine,
1517            self.registry.admission_audit(),
1518            &registration.task_queue,
1519            registration.node.as_deref(),
1520            &registration.identity,
1521            super::contracts::WorkerAdvertisement {
1522                activity_types: &activity_types,
1523                contracts: &advertised,
1524            },
1525        )
1526        .map_err(|error| LiminalServerError::ListenerAccept {
1527            message: error.to_string(),
1528        })
1529    }
1530
1531    /// Admit one connecting worker past the TRANSPORT's own refusals, or refuse
1532    /// it with a structured reason.
1533    ///
1534    /// Split out of [`ConnectionNotifier::on_worker_registered`] so every
1535    /// refusal this transport owns โ€” unbound supervisor, registry error, lease
1536    /// failure โ€” funnels through exactly one place that logs it. Before that
1537    /// split the rejection reason was carried in the `Rejected` ack and logged
1538    /// NOWHERE: on 2026-07-29 a worker whose `.v4` contract field-mismatched was
1539    /// refused on every single dial with the server silent on all of them, and
1540    /// the refused process looked merely asleep.
1541    ///
1542    /// **The contract check is deliberately NOT here.** That 2026-07-29 fix was
1543    /// made in this caller rather than at the shared gate, and within a week the
1544    /// gRPC transport re-manifested the identical silent refusal with nothing in
1545    /// the tree able to notice (#147). The gate now names its own refusal for
1546    /// every transport, so the contract check runs BEFORE this function and its
1547    /// error propagates from here already spoken for.
1548    fn admit_registration(
1549        &self,
1550        pid: u64,
1551        registration: &WireWorkerRegistration,
1552    ) -> Result<(), LiminalServerError> {
1553        // Admit through the SAME gate a gRPC worker passes: guard policy, each
1554        // namespace authorized, mint-or-gate, placement admission, then the
1555        // insert. A notifier with no admission bound refuses โ€” it never
1556        // inserts unguarded.
1557        let Some(admission) = &self.admission else {
1558            return Err(LiminalServerError::ListenerAccept {
1559                message: format!(
1560                    "liminal worker registration for connection {pid} rejected: this listener \
1561                     has no namespace admission bound, so it cannot judge which namespaces the \
1562                     worker may serve; a listener is commissioned with one before it accepts"
1563                ),
1564            });
1565        };
1566        if admission.auth_enabled {
1567            return Err(LiminalServerError::ListenerAccept {
1568                message: format!(
1569                    "liminal worker registration for connection {pid} (identity `{}`) rejected: \
1570                     this server authenticates callers (auth.enabled = true) and the liminal \
1571                     registration frame carries no credential to authenticate, so the worker's \
1572                     namespaces cannot be scoped to a caller; register this worker over gRPC \
1573                     with a bearer token, or run the server with auth disabled",
1574                    registration.identity
1575                ),
1576            });
1577        }
1578        // The delivery leg needs the supervisor to push to this connection. In
1579        // correct wiring it is bound before any connection is accepted; a missing
1580        // binding is a rejected registration, never a panic.
1581        let supervisor =
1582            self.supervisor
1583                .get()
1584                .ok_or_else(|| LiminalServerError::ListenerAccept {
1585                    message: format!(
1586                        "liminal worker registration for connection {pid} rejected: \
1587                     notifier supervisor handle not yet bound"
1588                    ),
1589                })?;
1590
1591        let delivery = WorkerDelivery::Liminal(LiminalWorkerDelivery::new(supervisor.clone(), pid));
1592        let caller = liminal_caller_identity(&registration.identity);
1593        let proto = proto_registration_from_wire(registration);
1594        let guard = bridge_admission(
1595            &admission.handle,
1596            self.registry.admit_delivery(
1597                &admission.guard,
1598                &caller,
1599                &proto,
1600                delivery,
1601                self.intervention_capabilities.clone(),
1602            ),
1603        )
1604        .map_err(|error| LiminalServerError::ListenerAccept {
1605            message: format!("liminal worker registration for connection {pid} rejected: {error}"),
1606        })?
1607        .map_err(|error| LiminalServerError::ListenerAccept {
1608            message: format!("liminal worker registration for connection {pid} rejected: {error}"),
1609        })?;
1610        let worker_id = guard
1611            .worker_id()
1612            .ok_or_else(|| LiminalServerError::ListenerAccept {
1613                message: format!(
1614                    "liminal worker registration for connection {pid} has no worker id"
1615                ),
1616            })?;
1617
1618        // OWN the guard for the connection's lifetime, keyed by pid. Dropping it
1619        // (on unregister) deregisters the worker, so the registration lives
1620        // exactly as long as the connection.
1621        let mut guards = self.guards.lock().map_err(|_| {
1622            // The accepted registry entry cannot be tracked for deregistration, so
1623            // reject (and drop the just-created guard, deregistering it) rather
1624            // than leak a never-deregistered association.
1625            LiminalServerError::ListenerAccept {
1626                message: format!(
1627                    "liminal worker registration for connection {pid} rejected: \
1628                     notifier guard map poisoned"
1629                ),
1630            }
1631        })?;
1632        guards.insert(pid, guard);
1633        drop(guards);
1634        if let Some(tracker) = &self.heartbeat_tracker
1635            && let Err(error) = tracker.register_connection(worker_id, std::time::Instant::now())
1636        {
1637            let removed = match self.guards.lock() {
1638                Ok(mut guards) => guards.remove(&pid),
1639                Err(poisoned) => poisoned.into_inner().remove(&pid),
1640            };
1641            drop(removed);
1642            return Err(LiminalServerError::ListenerAccept {
1643                message: format!(
1644                    "liminal worker registration for connection {pid} rejected: {error}"
1645                ),
1646            });
1647        }
1648        tracing::info!(
1649            connection_pid = pid,
1650            identity = %registration.identity,
1651            task_queue = %registration.task_queue,
1652            "registered liminal worker in-band"
1653        );
1654        Ok(())
1655    }
1656}
1657
1658/// The admission a liminal registration is judged by, bound at commissioning.
1659struct RegistrationAdmission {
1660    guard: NamespaceGuard,
1661    auth_enabled: bool,
1662    /// The runtime the async admission is bridged onto from the connection
1663    /// thread โ€” named at commissioning, never captured from ambient context.
1664    handle: tokio::runtime::Handle,
1665}
1666
1667/// Run the async admission to completion from the synchronous connection
1668/// callback, on whichever thread liminal invokes it from.
1669///
1670/// Three callers, three answers, no panic path:
1671/// - liminal's own connection thread (production) is on no runtime, so the
1672///   future is driven by `handle.block_on` directly;
1673/// - a multi-thread runtime worker (a test that drives the notifier from an
1674///   async body) cannot be blocked without stalling its scheduler, so the
1675///   worker is first handed back via `block_in_place`;
1676/// - a current-thread runtime worker has nothing to hand back to โ€” blocking
1677///   it would deadlock the very runtime the admission runs on โ€” so the call
1678///   is refused by name rather than hung.
1679///
1680/// # Errors
1681///
1682/// Returns the refusal for the current-thread case; the admission's own
1683/// verdict rides in the `Ok`.
1684fn bridge_admission<F: Future>(
1685    handle: &tokio::runtime::Handle,
1686    admission: F,
1687) -> Result<F::Output, ServerError> {
1688    match tokio::runtime::Handle::try_current() {
1689        Err(_) => Ok(handle.block_on(admission)),
1690        Ok(current) => match current.runtime_flavor() {
1691            tokio::runtime::RuntimeFlavor::MultiThread => {
1692                Ok(tokio::task::block_in_place(|| handle.block_on(admission)))
1693            }
1694            _ => Err(ServerError::Config {
1695                message: "the liminal registration callback ran on a current-thread Tokio \
1696                          runtime worker, which cannot be blocked while the namespace \
1697                          admission runs; drive the listener from a multi-thread runtime \
1698                          or from a plain thread, as liminal's connection process does"
1699                    .to_owned(),
1700            }),
1701        },
1702    }
1703}
1704
1705/// The caller a liminal registration is admitted as on an auth-off server:
1706/// the operator identity, subject = the frame's `identity` (or `operator`
1707/// when the frame left it empty) โ€” the liminal twin of the gRPC development
1708/// identity built from `x-aion-subject`.
1709fn liminal_caller_identity(identity: &str) -> CallerIdentity {
1710    let subject = identity.trim();
1711    CallerIdentity::operator(if subject.is_empty() {
1712        "operator"
1713    } else {
1714        subject
1715    })
1716}
1717
1718fn wire_activity_to_proto(
1719    activity: &liminal::protocol::WorkerActivityDescriptor,
1720) -> aion_proto::ProtoActivityDescriptor {
1721    aion_proto::ProtoActivityDescriptor {
1722        name: activity.name.clone(),
1723        input_schema_json: activity.input_schema_json.clone(),
1724        output_schema_json: activity.output_schema_json.clone(),
1725    }
1726}
1727
1728/// The liminal registration frame in the shape the namespace guard judges:
1729/// the same fields, the wire's `None` node as the proto's empty string.
1730fn proto_registration_from_wire(
1731    registration: &WireWorkerRegistration,
1732) -> aion_proto::ProtoRegisterWorker {
1733    aion_proto::ProtoRegisterWorker {
1734        namespaces: registration.namespaces.clone(),
1735        activity_types: registration.activity_types.clone(),
1736        task_queue: registration.task_queue.clone(),
1737        node: normalize_wire_node(registration.node.as_deref()).unwrap_or_default(),
1738        activities: registration
1739            .activities
1740            .iter()
1741            .map(wire_activity_to_proto)
1742            .collect(),
1743        identity: registration.identity.clone(),
1744        instance: None,
1745        // ABSENT, and absent means UNKNOWN rather than unlimited. The liminal
1746        // registration frame is a published protocol type whose six fields โ€”
1747        // namespaces, task queue, node, activity types, identity, activities โ€”
1748        // contain no capacity, so a liminal worker physically cannot state what
1749        // it runs at once in the frame that registers it. It is the only
1750        // transport in that position.
1751        //
1752        // The server does NOT fill the gap with a number of its own. It used to
1753        // stamp 1 here and have the worker SDK refuse any other configuration,
1754        // which made every liminal worker serial โ€” and once selection began
1755        // honouring capacity, that serialized the agent path that fleet
1756        // workflows fan across in parallel. The worker's CONFIGURED value now
1757        // arrives one frame later on `WORKER_CAPABILITIES_CHANNEL` and lands via
1758        // `ConnectedWorkerRegistry::set_advertised_capacity`; until then
1759        // selection passes the worker over.
1760        max_concurrency: None,
1761    }
1762}
1763
1764impl ConnectionNotifier for LiminalConnectionNotifier {
1765    fn on_worker_registered(
1766        &self,
1767        pid: u64,
1768        registration: &WireWorkerRegistration,
1769    ) -> Result<(), LiminalServerError> {
1770        // The contract gate NAMES its own refusal, for every transport, with the
1771        // queue, the node, the worker build identity and the structured reason.
1772        // So this path propagates that refusal SILENTLY rather than restating
1773        // it: a second copy of the same log is how #147 happened, and a log
1774        // written twice per refusal is #94's 12 MB/hour with a second author.
1775        self.validate_registration_contract(registration)?;
1776        // Every refusal this TRANSPORT owns is named here instead, at WARN, with
1777        // the same reason that rides back in the `Rejected` ack, so both sides'
1778        // logs carry identical text and a refused worker can be diagnosed from
1779        // either end.
1780        self.admit_registration(pid, registration)
1781            .inspect_err(|error| {
1782                // The two advertised sets are logged SEPARATELY and named for
1783                // what each one is. Logging `activity_types` alone beside a
1784                // contract-mismatch reason produced a record that listed an
1785                // action as advertised in the same line that reported it
1786                // `<missing>` โ€” true of two different sets, and unresolvable by
1787                // the operator it exists to help.
1788                let advertised_contracts = registration
1789                    .activities
1790                    .iter()
1791                    .map(|activity| activity.name.as_str())
1792                    .collect::<Vec<_>>();
1793                tracing::warn!(
1794                    connection_pid = pid,
1795                    identity = %registration.identity,
1796                    task_queue = %registration.task_queue,
1797                    namespaces = ?registration.namespaces,
1798                    advertised_activity_types = ?registration.activity_types,
1799                    advertised_contracts = ?advertised_contracts,
1800                    reason = %error,
1801                    "REFUSED liminal worker registration"
1802                );
1803            })
1804    }
1805
1806    fn on_worker_unregistered(&self, pid: u64) {
1807        // Remove + drop the guard for pid, deregistering the worker. A poisoned
1808        // lock on the close path has no peer to report to; recover the guard map
1809        // and still drop the guard so the registry does not keep routing to a
1810        // gone connection.
1811        let removed = match self.guards.lock() {
1812            Ok(mut guards) => guards.remove(&pid),
1813            Err(poisoned) => poisoned.into_inner().remove(&pid),
1814        };
1815        let worker_id = removed.as_ref().and_then(WorkerRegistration::worker_id);
1816        if let (Some(tracker), Some(worker_id)) = (&self.heartbeat_tracker, worker_id)
1817            && let Err(error) = tracker.unregister_connection(worker_id)
1818        {
1819            tracing::error!(
1820                %error,
1821                connection_pid = pid,
1822                worker_id = worker_id.value(),
1823                "failed to clear liminal worker connection lease"
1824            );
1825        }
1826        if removed.is_some() {
1827            tracing::info!(
1828                connection_pid = pid,
1829                "deregistered liminal worker on disconnect"
1830            );
1831        }
1832    }
1833
1834    fn on_channel_publish(&self, pid: u64, channel: &str, payload: &[u8]) -> bool {
1835        if let Some(tracker) = &self.heartbeat_tracker {
1836            let worker_id = match self.guards.lock() {
1837                Ok(guards) => guards.get(&pid).and_then(WorkerRegistration::worker_id),
1838                Err(poisoned) => poisoned
1839                    .into_inner()
1840                    .get(&pid)
1841                    .and_then(WorkerRegistration::worker_id),
1842            };
1843            if let Some(worker_id) = worker_id
1844                && let Err(error) =
1845                    tracker.record_connection_activity(worker_id, std::time::Instant::now())
1846            {
1847                tracing::error!(
1848                    %error,
1849                    connection_pid = pid,
1850                    worker_id = worker_id.value(),
1851                    "failed to advance liminal worker connection lease"
1852                );
1853            }
1854        }
1855        // Reserved liveness channel: refresh the beat's in-flight task stamp in
1856        // the shared heartbeat tracker (always consumed, never fanned out).
1857        if channel == WORKER_LIVENESS_CHANNEL {
1858            self.record_liveness_beat(pid, payload);
1859            return true;
1860        }
1861        // Reserved capabilities channel: apply the worker's advertised
1862        // intervention capabilities to its registered handle (always consumed).
1863        if channel == WORKER_CAPABILITIES_CHANNEL {
1864            self.record_capabilities_announcement(pid, payload);
1865            return true;
1866        }
1867        // Only consume the reserved observability channel; any other channel falls
1868        // through to liminal's normal fan-out (this returns false).
1869        if channel != liminal_sdk::OBSERVABILITY_CHANNEL {
1870            return false;
1871        }
1872        let Some(tap) = &self.transcript else {
1873            // No transcript sequencer installed (a non-agent deployment): still
1874            // CONSUME the reserved channel so it never leaks into the fan-out, but
1875            // drop the event โ€” there is nothing to persist it into.
1876            return true;
1877        };
1878        let event: aion_core::ActivityEvent = match serde_json::from_slice(payload) {
1879            Ok(event) => event,
1880            Err(error) => {
1881                tracing::warn!(%error, "observability tap: malformed ActivityEvent payload");
1882                return true;
1883            }
1884        };
1885        // Hand the event to the ordered drain queue (the synchronous
1886        // connection-process callback never blocks). A full queue means the
1887        // durable append cannot keep up: the event is dropped with a warning,
1888        // never buffered without bound.
1889        if let Err(error) = tap.queue.try_send(event) {
1890            tracing::warn!(%error, "observability tap: transcript queue rejected event");
1891        }
1892        true
1893    }
1894}
1895
1896/// The production [`InterventionTransport`](super::intervention::InterventionTransport):
1897/// pushes a routed command to the owning worker over its liminal server-push
1898/// connection (NOI-6, ยง6.2).
1899///
1900/// It reads the worker handle's [`WorkerDelivery::Liminal`] leg and pushes the
1901/// neutral [`InterventionRequest`] via [`LiminalWorkerDelivery::push_intervention`],
1902/// running the blocking push off the async runtime. A worker delivered over gRPC
1903/// (no liminal leg) surfaces the stale-target no-op via a connection-lost error, so
1904/// the router NACKs the operator rather than routing to a leg it cannot reach.
1905#[derive(Clone, Debug, Default)]
1906pub struct LiminalInterventionTransport;
1907
1908#[async_trait]
1909impl super::intervention::InterventionTransport for LiminalInterventionTransport {
1910    async fn push(
1911        &self,
1912        worker: &super::registry::WorkerHandle,
1913        command: aion_core::InterventionCommand,
1914    ) -> Result<aion_core::InterventionOutcome, ServerError> {
1915        let delivery = match worker.delivery() {
1916            WorkerDelivery::Liminal(delivery) => delivery.clone(),
1917            WorkerDelivery::Grpc(_) => {
1918                // No liminal leg to push to: the intervention transport rides the
1919                // liminal push channel only, so this is unreachable for the target.
1920                return Err(ServerError::worker_connection_lost(
1921                    "liminal-push",
1922                    "owning worker is not delivered over liminal".to_owned(),
1923                ));
1924            }
1925        };
1926        let request = InterventionRequest {
1927            intervention: command,
1928        };
1929        let reply = tokio::task::spawn_blocking(move || delivery.push_intervention(&request))
1930            .await
1931            .map_err(|error| {
1932                dispatch_error(
1933                    "liminal-push",
1934                    format!("intervention task join failed: {error}"),
1935                )
1936            })??;
1937        Ok(reply.outcome)
1938    }
1939}
1940
1941#[cfg(test)]
1942#[path = "liminal_contract_tests.rs"]
1943mod contract_tests;
1944
1945#[cfg(test)]
1946mod tests {
1947    use super::{
1948        AnnouncementProbe, DispatchRequest, LiminalServerError, OUTBOUND_BOUND_KEY,
1949        channel_for_row, classify_push_error, classify_reply_error, dispatch_channel_name,
1950        normalize_wire_node,
1951    };
1952
1953    /// The oversize refusal reaches the REPLY awaiter, not the push call: liminal
1954    /// reserves the slot synchronously and measures the frame afterwards in the
1955    /// connection process. So the reply classifier must name it unservable
1956    /// too โ€” this arm used to wrap it as the retryable dispatch class, which is
1957    /// why the operator's refusal said "retryable" for a frame no retry could
1958    /// carry.
1959    #[test]
1960    fn an_oversize_frame_on_the_reply_awaiter_is_unservable_and_names_the_bound() {
1961        let error = LiminalServerError::PushFrameExceedsOutboundCapacity {
1962            correlation_id: 11,
1963            needed: 5_089_012,
1964            capacity: 1_048_576,
1965            queued: 0,
1966        };
1967        let classified = classify_reply_error(&error);
1968        assert!(classified.is_worker_dispatch_unservable(), "{classified}");
1969        assert!(!classified.is_worker_connection_lost(), "{classified}");
1970        let said = classified.to_string();
1971        for needle in ["5089012", "1048576", OUTBOUND_BOUND_KEY] {
1972            assert!(said.contains(needle), "refusal must name {needle}: {said}");
1973        }
1974    }
1975
1976    /// The reply classifier keeps its two prior readings exactly: a closed
1977    /// connection is the TRANSPORT-loss class (fail over and retry), and any
1978    /// other reply fault stays the retryable dispatch class.
1979    #[test]
1980    fn a_closed_connection_on_the_reply_awaiter_is_still_a_lost_connection() {
1981        let classified =
1982            classify_reply_error(&LiminalServerError::PushReplyDisconnected { correlation_id: 3 });
1983        assert!(classified.is_worker_connection_lost(), "{classified}");
1984        assert!(!classified.is_worker_dispatch_unservable(), "{classified}");
1985    }
1986
1987    #[test]
1988    fn any_other_reply_fault_stays_the_retryable_dispatch_class() {
1989        let classified =
1990            classify_reply_error(&LiminalServerError::PushReplyExpired { correlation_id: 5 });
1991        assert!(
1992            matches!(classified, crate::error::ServerError::WorkerDispatch { .. }),
1993            "{classified}"
1994        );
1995        assert!(!classified.is_worker_dispatch_unservable(), "{classified}");
1996        assert!(!classified.is_worker_connection_lost(), "{classified}");
1997    }
1998
1999    /// The oversize frame is classified UNSERVABLE, never as a lost connection:
2000    /// the lost arm means "fail over and retry", and retrying a frame that can
2001    /// never fit is the 846-lease fault this arm exists to end. The refusal
2002    /// names both numbers and the operator key.
2003    #[test]
2004    fn an_oversize_frame_is_unservable_and_names_the_bound_and_the_key() {
2005        let error = LiminalServerError::PushFrameExceedsOutboundCapacity {
2006            correlation_id: 7,
2007            needed: 6_214_149,
2008            capacity: 4_194_304,
2009            queued: 0,
2010        };
2011        let classified = classify_push_error(&error);
2012        assert!(classified.is_worker_dispatch_unservable(), "{classified}");
2013        assert!(!classified.is_worker_connection_lost(), "{classified}");
2014        assert!(!classified.is_worker_busy(), "{classified}");
2015        let said = classified.to_string();
2016        for needle in ["6214149", "4194304", OUTBOUND_BOUND_KEY] {
2017            assert!(said.contains(needle), "refusal must name {needle}: {said}");
2018        }
2019    }
2020
2021    /// The two pre-existing classes are untouched: a cap refusal is still BUSY
2022    /// (attempt-neutral park) and every other push failure is still LOST
2023    /// (immediate failover). The new arm narrows nothing but itself.
2024    #[test]
2025    fn the_cap_and_lost_classes_are_unchanged() {
2026        let cap = LiminalServerError::ConnectionCapReached {
2027            operation: "push".to_owned(),
2028            cap: "max_pending_pushes_per_connection",
2029            limit: 32,
2030        };
2031        assert!(classify_push_error(&cap).is_worker_busy());
2032        let other = LiminalServerError::PushReplyDisconnected { correlation_id: 9 };
2033        assert!(classify_push_error(&other).is_worker_connection_lost());
2034    }
2035
2036    /// The key named in the refusal is the key on the config struct: one
2037    /// spelling, pinned, so the operator is never sent to a field that does not
2038    /// exist.
2039    #[test]
2040    fn the_refusal_names_the_real_config_key() -> Result<(), Box<dyn std::error::Error>> {
2041        // Go through serde's field name rather than trusting a second string:
2042        // the struct is `Deserialize`, so a TOML document naming the key must
2043        // land on the field, or the refusal is sending operators to a key that
2044        // does not exist.
2045        let parsed: crate::config::OutboxConfig =
2046            toml::from_str(&format!("{OUTBOUND_BOUND_KEY} = 1"))?;
2047        assert_eq!(parsed.liminal_max_connection_outbound_bytes, Some(1));
2048        Ok(())
2049    }
2050
2051    /// STRUCTURAL TWIN GUARD. `aion_worker::runtime::liminal::DispatchRequest`
2052    /// is a field-for-field mirror of this crate's, kept by hand because the
2053    /// worker crate does not depend on the server. Until now nothing checked
2054    /// it. This does, at compile time (the worker type must exist and decode)
2055    /// and at run time: a frame this side writes re-encodes byte-for-byte from
2056    /// the worker type, the two key sets are equal, and the payload-bytes
2057    /// codec is the same on both sides, so a base64 `input` decodes there to
2058    /// the bytes it decodes to here.
2059    #[test]
2060    fn the_dispatch_request_twins_agree_field_for_field() -> Result<(), Box<dyn std::error::Error>>
2061    {
2062        use base64::Engine as _;
2063        use std::collections::BTreeSet;
2064
2065        let ours = DispatchRequest {
2066            activity_type: "charge-card".to_owned(),
2067            workflow_id: WorkflowId::new(Uuid::new_v4()),
2068            ordinal: 7,
2069            run_id: Some(RunId::new(Uuid::new_v4())),
2070            completion_token: "generation-3".to_owned(),
2071            idempotency_key: "effect-key".to_owned(),
2072            input: br#"{"amount":42}"#.to_vec(),
2073            attempt: 3,
2074            labels: std::collections::BTreeMap::from([("region".to_owned(), "apac".to_owned())]),
2075            heartbeat_window_ms: 30_000,
2076        };
2077        let wire = serde_json::to_value(&ours)?;
2078        let theirs: aion_worker::runtime::liminal::DispatchRequest =
2079            serde_json::from_value(wire.clone())?;
2080        assert_eq!(
2081            serde_json::to_value(&theirs)?,
2082            wire,
2083            "the worker twin must re-encode this frame identically"
2084        );
2085        let keys = |value: &serde_json::Value| -> BTreeSet<String> {
2086            value
2087                .as_object()
2088                .map(|object| object.keys().cloned().collect())
2089                .unwrap_or_default()
2090        };
2091        assert_eq!(keys(&wire), keys(&serde_json::to_value(&theirs)?));
2092        assert_eq!(theirs.input, ours.input);
2093        assert_eq!(theirs.completion_token, ours.completion_token);
2094        assert_eq!(theirs.attempt, ours.attempt);
2095        assert_eq!(theirs.heartbeat_window_ms, ours.heartbeat_window_ms);
2096
2097        let mut as_base64 = wire;
2098        as_base64["input"] = serde_json::Value::String(
2099            base64::engine::general_purpose::STANDARD.encode(&ours.input),
2100        );
2101        let ours_from_base64: DispatchRequest = serde_json::from_value(as_base64.clone())?;
2102        let theirs_from_base64: aion_worker::runtime::liminal::DispatchRequest =
2103            serde_json::from_value(as_base64)?;
2104        assert_eq!(ours_from_base64, ours);
2105        assert_eq!(theirs_from_base64.input, ours.input);
2106        Ok(())
2107    }
2108    use crate::worker::registry::RegistrationOptions;
2109    use aion_core::{ActivityId, ContentType, Payload, RunId, WorkflowId};
2110    use aion_store::{OutboxRow, OutboxStatus};
2111    use chrono::Utc;
2112    use uuid::Uuid;
2113
2114    /// ABSENT is not MALFORMED, and the tap has to be able to tell them apart.
2115    ///
2116    /// `max_concurrency` is a required field of the announcement, so a worker
2117    /// that predates it produced a `serde` error and the tap logged "malformed
2118    /// payload" โ€” a message that names the wrong problem and says nothing about
2119    /// the consequence, which is that the server holds that worker at capacity
2120    /// unknown and will never select it for a dispatch. The probe decodes the
2121    /// absence instead of failing on it, which is what lets the tap say so.
2122    #[test]
2123    fn an_announcement_without_capacity_decodes_as_absent_not_as_malformed()
2124    -> Result<(), Box<dyn std::error::Error>> {
2125        let pre_capacity = br#"{"capabilities":{"supported":[]}}"#;
2126        let decoded: AnnouncementProbe = serde_json::from_slice(pre_capacity)?;
2127        assert!(
2128            decoded.max_concurrency.is_none(),
2129            "the absence must be readable as absence, so the tap can name the condition and its \
2130             consequence rather than reporting a parse failure"
2131        );
2132
2133        // A CURRENT announcement still carries the number through.
2134        let current = br#"{"capabilities":{"supported":[]},"max_concurrency":3}"#;
2135        let decoded: AnnouncementProbe = serde_json::from_slice(current)?;
2136        assert_eq!(decoded.max_concurrency, Some(3));
2137
2138        // And genuine rubbish is still a decode failure, so tolerating the
2139        // missing field did not turn the probe into something that accepts
2140        // anything.
2141        assert!(
2142            serde_json::from_slice::<AnnouncementProbe>(br#"{"capabilities":7}"#).is_err(),
2143            "a malformed payload must still fail to decode"
2144        );
2145        Ok(())
2146    }
2147
2148    /// The NOI-6 dispatch owner guard RELEASES its binding on drop, on EVERY exit path
2149    /// (reply, error, panic) โ€” so the attempt-owner back-index tracks exactly the
2150    /// attempts currently in flight. This is the invariant the dispatch path relies on
2151    /// to never leak a finished attempt's owner.
2152    #[tokio::test]
2153    async fn attempt_owner_guard_releases_on_drop() -> Result<(), Box<dyn std::error::Error>> {
2154        use super::super::intervention::{AttemptKey, AttemptOwnerIndex};
2155        use super::super::registry::{ConnectedWorkerRegistry, WorkerDelivery};
2156        use super::AttemptOwnerGuard;
2157
2158        // A real registration yields a real WorkerId (there is no fabricated id).
2159        let registry = ConnectedWorkerRegistry::default();
2160        let (tx, _rx) = tokio::sync::mpsc::channel(1);
2161        let types = [String::from("agent")];
2162        let registration = registry.register_delivery(
2163            [String::from("default")],
2164            String::from("default"),
2165            None,
2166            types.iter(),
2167            WorkerDelivery::Grpc(tx),
2168            RegistrationOptions::identified(
2169                String::from("liminal-transport-worker"),
2170                crate::worker::UNBOUNDED_SENDER_WORKER_CONCURRENCY,
2171            )
2172            .with_intervention_capabilities(aion_core::InterventionCapabilities::none()),
2173        )?;
2174        let worker = registration
2175            .worker_id()
2176            .ok_or("registration must assign a worker id")?;
2177
2178        let owners = AttemptOwnerIndex::new();
2179        let key = AttemptKey::new(
2180            WorkflowId::new(Uuid::nil()),
2181            aion_core::RunId::new(Uuid::from_u128(0x11)),
2182            ActivityId::from_sequence_position(3),
2183            1,
2184        );
2185        owners.bind(key.clone(), worker);
2186        assert_eq!(
2187            owners.owner(&key),
2188            Some(worker),
2189            "owner bound before the guard"
2190        );
2191        {
2192            let _guard = AttemptOwnerGuard {
2193                owners: owners.clone(),
2194                key: key.clone(),
2195            };
2196            assert_eq!(
2197                owners.owner(&key),
2198                Some(worker),
2199                "still bound while in flight"
2200            );
2201        }
2202        // The guard dropped at the end of the block: the binding is released, so a
2203        // later intervention resolves no owner (the too-late no-op).
2204        assert_eq!(
2205            owners.owner(&key),
2206            None,
2207            "owner released when the dispatch returns"
2208        );
2209        Ok(())
2210    }
2211
2212    /// The channel format is pinned EXACTLY: any change is a wire-compatibility
2213    /// break (the dispatcher and any worker subscription must agree byte-for-byte).
2214    /// The UNPINNED (`None`) channel MUST stay byte-identical to the pre-NODE-5
2215    /// format so existing pool subscriptions are stable.
2216    #[test]
2217    fn channel_format_is_pinned() {
2218        assert_eq!(
2219            dispatch_channel_name("remote", "gpu", None),
2220            "aion.dispatch.remote.gpu"
2221        );
2222        assert_eq!(
2223            dispatch_channel_name("local", "norn", None),
2224            "aion.dispatch.local.norn"
2225        );
2226    }
2227
2228    /// A node-pinned dispatch appends the node as an injectively-encoded
2229    /// sub-segment: `f(ns, tq, Some(node))` == `aion.dispatch.{ns}.{tq}.{node}`.
2230    #[test]
2231    fn node_pinned_channel_appends_node_subsegment() {
2232        assert_eq!(
2233            dispatch_channel_name("remote", "gpu", Some("box-7")),
2234            "aion.dispatch.remote.gpu.box-7"
2235        );
2236    }
2237
2238    /// Same input always yields the same channel (the function is stable/total),
2239    /// for both the unpinned and node-pinned cases.
2240    #[test]
2241    fn channel_derivation_is_stable() {
2242        assert_eq!(
2243            dispatch_channel_name("default", "default", None),
2244            dispatch_channel_name("default", "default", None)
2245        );
2246        assert_eq!(
2247            dispatch_channel_name("default", "default", Some("box-1")),
2248            dispatch_channel_name("default", "default", Some("box-1"))
2249        );
2250    }
2251
2252    /// Distinct `(namespace, task_queue)` pools derive distinct channels โ€” the
2253    /// whole point of NSTQ-5: `(remote, gpu)` and `(local, norn)` never collide.
2254    #[test]
2255    fn distinct_pools_get_distinct_channels() {
2256        assert_ne!(
2257            dispatch_channel_name("remote", "gpu", None),
2258            dispatch_channel_name("local", "norn", None)
2259        );
2260    }
2261
2262    /// A node-pinned dispatch and the unpinned dispatch for the SAME pool derive
2263    /// DISTINCT channels, and two distinct nodes for the same pool also differ โ€”
2264    /// the property node isolation rests on (the subscriber contract).
2265    #[test]
2266    fn node_pin_separates_channels() {
2267        let unpinned = dispatch_channel_name("remote", "gpu", None);
2268        let box7 = dispatch_channel_name("remote", "gpu", Some("box-7"));
2269        let box8 = dispatch_channel_name("remote", "gpu", Some("box-8"));
2270        assert_ne!(
2271            unpinned, box7,
2272            "pinned dispatch must not reach unpinned pool"
2273        );
2274        assert_ne!(box7, box8, "distinct nodes must not collide");
2275    }
2276
2277    /// The core injectivity property: free-form fields containing the segment
2278    /// separator `.` must NOT bleed across the join. With the raw `format!` the
2279    /// disjoint pools `("a.b", "c")` and `("a", "b.c")` both collapsed onto
2280    /// `aion.dispatch.a.b.c` โ€” a cross-pool/cross-namespace leak. The per-segment
2281    /// encode keeps them distinct.
2282    #[test]
2283    fn dotted_fields_do_not_collide_across_segments() {
2284        assert_ne!(
2285            dispatch_channel_name("a.b", "c", None),
2286            dispatch_channel_name("a", "b.c", None),
2287            "a '.' in a field must not bleed across the segment separator"
2288        );
2289    }
2290
2291    /// Injectivity holds ACROSS segment counts: a 2-segment (unpinned) channel
2292    /// can never be confused with a 3-segment (node-pinned) channel even when a
2293    /// `.` in a field would otherwise make the raw strings line up. Both
2294    /// directions of the brief's collision cases must stay distinct.
2295    #[test]
2296    fn node_subsegment_does_not_collide_with_dotted_fields() {
2297        // A node sub-segment vs the same dot living inside task_queue.
2298        assert_ne!(
2299            dispatch_channel_name("a", "b", Some("c")),
2300            dispatch_channel_name("a", "b.c", None),
2301            "a node sub-segment must not collide with a dotted task_queue"
2302        );
2303        // The dot living inside namespace vs a node sub-segment.
2304        assert_ne!(
2305            dispatch_channel_name("a.b", "c", None),
2306            dispatch_channel_name("a", "b", Some("c")),
2307            "a dotted namespace must not collide with a node-pinned channel"
2308        );
2309    }
2310
2311    /// More reserved-char shifts that the raw `format!` collapsed but the encode
2312    /// must keep distinct โ€” the dot can sit on either side of the boundary.
2313    #[test]
2314    fn reserved_char_shifts_stay_distinct() {
2315        // Dot at the end of namespace vs start of task_queue.
2316        assert_ne!(
2317            dispatch_channel_name("ns.", "tq", None),
2318            dispatch_channel_name("ns", ".tq", None)
2319        );
2320        // Empty field vs the dot living in the other field.
2321        assert_ne!(
2322            dispatch_channel_name("", "a.b", None),
2323            dispatch_channel_name(".a", "b", None)
2324        );
2325        // The escape char itself must not let a literal `%2E` impersonate an
2326        // encoded `.`: `("%2E", "x")` (literal percent-two-E) must differ from
2327        // `(".", "x")` (an actual dot, which encodes to `%2E`).
2328        assert_ne!(
2329            dispatch_channel_name("%2E", "x", None),
2330            dispatch_channel_name(".", "x", None)
2331        );
2332    }
2333
2334    /// Encoding is injective in ALL THREE segments independently and is exactly
2335    /// reversible (the property the channel relies on), so a small exhaustive
2336    /// sweep of reserved-char arrangements โ€” INCLUDING the optional node taking
2337    /// `None` and every reserved-char value โ€” yields all-distinct channels. This
2338    /// covers cross-segment-count collisions (the `None` vs `Some` boundary) too.
2339    #[test]
2340    fn encoding_is_injective_over_reserved_char_triples() {
2341        let fields = ["a", "a.b", "a.", ".a", ".", "", "%", "%2E", "a%b", "%2."];
2342        let nodes = [
2343            None,
2344            Some("a"),
2345            Some("a.b"),
2346            Some("."),
2347            Some(""),
2348            Some("%2E"),
2349        ];
2350        let mut channels = std::collections::HashSet::new();
2351        for ns in fields {
2352            for tq in fields {
2353                for node in nodes {
2354                    let channel = dispatch_channel_name(ns, tq, node);
2355                    assert!(
2356                        channels.insert(channel.clone()),
2357                        "collision on ({ns:?}, {tq:?}, {node:?}) -> {channel}"
2358                    );
2359                }
2360            }
2361        }
2362    }
2363
2364    fn row(namespace: &str, task_queue: &str) -> OutboxRow {
2365        let workflow_id = WorkflowId::new(Uuid::new_v4());
2366        OutboxRow {
2367            dispatch_key: format!("{workflow_id}:0"),
2368            workflow_id,
2369            ordinal: 0,
2370            run_id: Some(aion_core::RunId::new_v4()),
2371            namespace: namespace.to_owned(),
2372            task_queue: task_queue.to_owned(),
2373            node: None,
2374            activity_type: "charge-card".to_owned(),
2375            input: Payload::new(ContentType::Json, Vec::new()),
2376            status: OutboxStatus::Pending,
2377            attempt: 0,
2378            started_attempt: 1,
2379            visible_after: Utc::now(),
2380            claimed_at: None,
2381            failure_delivered: false,
2382        }
2383    }
2384
2385    /// A row's channel is derived from its durable `(namespace, task_queue)`
2386    /// columns (NSTQ-2), through the same single derivation function โ€” and
2387    /// `activity_type` does NOT enter the channel. With `node = None` the channel
2388    /// is byte-identical to the pre-NODE-5 2-segment form.
2389    #[test]
2390    fn channel_for_row_uses_namespace_and_task_queue_only() {
2391        let remote_gpu = row("remote", "gpu");
2392        let local_norn = row("local", "norn");
2393        assert_eq!(channel_for_row(&remote_gpu), "aion.dispatch.remote.gpu");
2394        assert_eq!(channel_for_row(&local_norn), "aion.dispatch.local.norn");
2395        assert_ne!(channel_for_row(&remote_gpu), channel_for_row(&local_norn));
2396
2397        // Two rows that differ ONLY in activity_type derive the SAME channel:
2398        // activity_type is matched after delivery, not used to select the pool.
2399        let mut other_activity = row("remote", "gpu");
2400        other_activity.activity_type = "refund".to_owned();
2401        assert_eq!(
2402            channel_for_row(&remote_gpu),
2403            channel_for_row(&other_activity),
2404            "activity_type must not affect the channel"
2405        );
2406    }
2407
2408    /// A row carrying `Some(node)` (NODE-2) derives the node-pinned sub-channel,
2409    /// distinct from the same pool's unpinned channel; a row with `None` derives
2410    /// the 2-segment channel. `channel_for_row` threads `row.node` through the
2411    /// single derivation function.
2412    #[test]
2413    fn channel_for_row_derives_node_subchannel_when_pinned() {
2414        let mut pinned = row("remote", "gpu");
2415        pinned.node = Some("box-7".to_owned());
2416        assert_eq!(channel_for_row(&pinned), "aion.dispatch.remote.gpu.box-7");
2417
2418        let unpinned = row("remote", "gpu");
2419        assert_eq!(channel_for_row(&unpinned), "aion.dispatch.remote.gpu");
2420        assert_ne!(channel_for_row(&pinned), channel_for_row(&unpinned));
2421    }
2422
2423    /// The wire `node` is normalized onto the registry's optional affinity with
2424    /// the SAME none-convention the gRPC registration path uses: `None` and the
2425    /// empty-string node both collapse to unpinned (`None`), a non-empty value is
2426    /// the advertised node. An empty-string node must NOT register a distinct
2427    /// empty affinity no pinned dispatch could match.
2428    #[test]
2429    fn wire_node_normalizes_empty_to_none() {
2430        assert_eq!(normalize_wire_node(None), None);
2431        assert_eq!(normalize_wire_node(Some("")), None);
2432        assert_eq!(normalize_wire_node(Some("box-7")), Some("box-7".to_owned()));
2433    }
2434}