Skip to main content

liminal_server/server/connection/
services.rs

1use std::collections::HashMap;
2use std::path::Path;
3use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
4use std::sync::{Arc, Mutex, OnceLock, RwLock, mpsc};
5use std::time::Instant;
6
7use haematite::{Database, DatabaseConfig, EventStore};
8use liminal::channel::{ChannelConfig, ChannelHandle, ChannelMode, ChannelSupervisor, Schema};
9use liminal::conversation::{
10    ConversationSupervisor, CrashPolicy, EchoBehaviour, ParticipantBehaviour,
11};
12use liminal::durability::bridge::block_on;
13use liminal::durability::{
14    DedupCache, DedupDecision, DurabilityError, DurableStore, EphemeralHaematiteStore,
15    HaematiteStore, ProcessingReceipt, open_ephemeral,
16};
17use liminal::protocol::{MessageEnvelope, ProtocolError, SchemaId as ProtocolSchemaId};
18
19use super::channel_registry::{
20    ChannelAccessError, ChannelBuildError, ChannelConfigField, ChannelDescriptor, ChannelOrigin,
21    ChannelRegistration, ChannelRegistryError, ChannelState, ChannelStatus, MAX_CHANNELS_KEY,
22    Registered, STATE_ACTIVE, STATE_QUIESCED, UNRECORDED_QUIESCE_REASON,
23};
24use super::conversation::{ConnectionConversation, LiminalConversationResource};
25use super::services_cluster::build_channel_cluster;
26use super::services_schema::{ChannelSchema, resolve_channel_schema, resolve_schema_bytes};
27use super::worker_front_door::WorkerFrontDoorServices;
28use crate::ServerError;
29use crate::config::types::{ClusterConfig, ServerConfig, ServiceProfile};
30use crate::health::reissue::OperatorCredentialReissuer;
31use crate::health::unloadable::UnloadableConversationRecord;
32use crate::server::participant::{InstalledParticipantService, ProductionParticipantHandler};
33
34pub use super::services_cluster::ChannelCluster;
35
36/// Registry of custom conversation responders, keyed by conversation subject.
37///
38/// A registered [`ParticipantBehaviour`] becomes the participant for any
39/// conversation opened on its subject; subjects with no entry fall back to the
40/// built-in [`EchoBehaviour`].
41type ResponderRegistry = HashMap<String, Arc<dyn ParticipantBehaviour>>;
42
43/// Marker for resources retained by a connection process until unsubscribe.
44pub trait SubscriptionResource: std::fmt::Debug + Send {
45    /// Releases the library subscription resource.
46    ///
47    /// # Errors
48    /// Returns [`ServerError`] when the liminal library reports an unsubscribe failure.
49    fn unsubscribe(self: Box<Self>) -> Result<(), ServerError>;
50
51    /// Attempts to pull the next delivered envelope from the wrapped library
52    /// subscription without blocking.
53    ///
54    /// Returns `None` when the subscriber inbox is empty (or momentarily
55    /// unavailable): the connection process is the delivery pump, so a transient
56    /// empty read is simply "nothing to deliver this slice", never an error.
57    fn try_next(&mut self) -> Option<liminal::envelope::Envelope>;
58
59    /// Non-consuming availability query for the post-arm race barrier.
60    fn has_pending(&self) -> bool;
61
62    /// Whether an overflow has marked this subscription for shedding (§5). The
63    /// delivery pump sheds an overflowed subscription with a typed error frame.
64    /// Defaulted to `false`: a resource with no bounded inbox never overflows.
65    fn is_overflowed(&self) -> bool {
66        false
67    }
68}
69
70/// Library subscription resource owned by a single connection process.
71#[derive(Debug)]
72pub struct ConnectionSubscription {
73    id: u64,
74    /// Channel this subscription was opened on, carried for DIAGNOSTICS only —
75    /// the shed warning has to name the channel or an operator reading it learns
76    /// that something was shed without learning what. Empty when unset (the
77    /// scheduler-free unit fixtures, which construct through `new` and never log).
78    channel: String,
79    /// Client-chosen application stream the server delivers this subscription's
80    /// messages on (echoed on `SubscribeAck`, carried on every `Deliver`). Set by
81    /// the connection process from the `Subscribe` frame before the subscription is
82    /// stored; `0` only while momentarily unset during construction.
83    stream_id: u32,
84    selected_schema: ProtocolSchemaId,
85    resource: Box<dyn SubscriptionResource>,
86}
87
88impl ConnectionSubscription {
89    /// Creates an owned subscription resource for one connection process.
90    #[must_use]
91    pub fn new(
92        id: u64,
93        selected_schema: ProtocolSchemaId,
94        resource: Box<dyn SubscriptionResource>,
95    ) -> Self {
96        Self {
97            id,
98            channel: String::new(),
99            stream_id: 0,
100            selected_schema,
101            resource,
102        }
103    }
104
105    /// Records the channel this subscription was opened on (diagnostics).
106    ///
107    /// Additive setter rather than a fourth `new` parameter: `new` is public API
108    /// and its existing callers keep compiling byte-for-byte.
109    pub(super) fn set_channel(&mut self, channel: String) {
110        self.channel = channel;
111    }
112
113    /// The channel this subscription was opened on, or `""` when unset.
114    #[must_use]
115    pub(super) fn channel(&self) -> &str {
116        &self.channel
117    }
118
119    /// Returns the protocol subscription id.
120    #[must_use]
121    pub const fn id(&self) -> u64 {
122        self.id
123    }
124
125    /// Records the client-chosen delivery stream id for this subscription.
126    pub(super) const fn set_stream_id(&mut self, stream_id: u32) {
127        self.stream_id = stream_id;
128    }
129
130    /// Returns the client-chosen application stream id deliveries ride on.
131    #[must_use]
132    pub(super) const fn stream_id(&self) -> u32 {
133        self.stream_id
134    }
135
136    /// Returns the schema selected for this subscription stream.
137    #[must_use]
138    pub const fn selected_schema(&self) -> ProtocolSchemaId {
139        self.selected_schema
140    }
141
142    /// Attempts to pull the next delivered envelope without blocking.
143    pub(super) fn try_next(&mut self) -> Option<liminal::envelope::Envelope> {
144        self.resource.try_next()
145    }
146
147    pub(super) fn has_pending(&self) -> bool {
148        self.resource.has_pending()
149    }
150
151    /// Whether this subscription has been shed by an inbox overflow (§5).
152    pub(super) fn is_overflowed(&self) -> bool {
153        self.resource.is_overflowed()
154    }
155
156    pub(super) fn unsubscribe(self) -> Result<(), ServerError> {
157        self.resource.unsubscribe()
158    }
159}
160
161/// Outcome of a server publish.
162///
163/// Carries the assigned message id plus a genuine delivery ack (`delivered` = the
164/// message was accepted by at least one live subscriber on this publish, after any
165/// dedup-on-delivery suppression).
166#[derive(Clone, Copy, Debug, PartialEq, Eq)]
167pub struct PublishOutcome {
168    /// Monotonic message id assigned to the accepted publish.
169    pub message_id: u64,
170    /// Whether the message was genuinely delivered to a subscriber. `false` means
171    /// the publish was accepted but reached no subscriber (empty channel) or was
172    /// a duplicate suppressed by dedup-on-delivery.
173    pub delivered: bool,
174}
175
176/// Which channel operation is asking the roster for admission.
177///
178/// `Copy`, so the hot path allocates nothing to ask. The contrast is
179/// [`ServerError::UnsupportedOperation`]'s owned `operation: String`: that one is
180/// built on a refusal path and pays for its allocation once per refusal, while
181/// this one is consulted on every publish and every subscribe frame that reaches
182/// the connection process.
183#[derive(Clone, Copy, Debug, PartialEq, Eq)]
184pub enum ChannelOperation {
185    /// A `Publish` frame is asking.
186    Publish,
187    /// A `Subscribe` frame is asking.
188    Subscribe,
189}
190
191/// Operations that adapt wire frames to liminal library calls.
192pub trait ConnectionServices: std::fmt::Debug + Send + Sync {
193    /// Returns the complete participant service installed on this adapter.
194    ///
195    /// `None` keeps participant capability disabled even when the adapter owns a
196    /// durable store for unrelated channel traffic. The returned token is
197    /// server-sealed and atomically carries declared semantics plus durability,
198    /// making a handler-without-store activation impossible by construction.
199    fn participant_service(&self) -> Option<InstalledParticipantService> {
200        None
201    }
202
203    /// Delegates a publish request to the liminal library.
204    ///
205    /// `idempotency_key`, when `Some`, drives dedup-on-delivery: a re-publish with
206    /// the same key is delivered to subscribers at most once. The returned
207    /// [`PublishOutcome`] carries the genuine delivery ack.
208    ///
209    /// # Errors
210    /// Returns [`ServerError`] when the liminal publish operation fails.
211    fn publish(
212        &self,
213        channel: &str,
214        envelope: &MessageEnvelope,
215        idempotency_key: Option<&str>,
216    ) -> Result<PublishOutcome, ServerError>;
217
218    /// Delegates a subscribe request to the liminal library.
219    ///
220    /// `install`, when `Some`, carries the connection's §5 shared inbox byte
221    /// budget, per-inbox fairness cap, and R3 wake notifier. The implementation
222    /// MUST install it on the subscription's inbox BEFORE the registration is
223    /// published to the channel actor (i.e. before any envelope can be
224    /// delivered), so no envelope is ever admitted uncharged, past the depth
225    /// cap, or without a wake. Implementations with no real inbox (test
226    /// stand-ins, capability-scoped profiles that refuse subscribe) may ignore
227    /// it.
228    ///
229    /// # Errors
230    /// Returns [`ServerError`] when the liminal subscribe operation fails.
231    fn subscribe(
232        &self,
233        channel: &str,
234        accepted_schemas: &[ProtocolSchemaId],
235        install: Option<liminal::channel::InboxInstall>,
236    ) -> Result<ConnectionSubscription, ServerError>;
237
238    /// Delegates unsubscribe to the liminal library.
239    ///
240    /// # Errors
241    /// Returns [`ServerError`] when the liminal unsubscribe operation fails.
242    fn unsubscribe(&self, subscription: ConnectionSubscription) -> Result<(), ServerError>;
243
244    /// Delegates conversation open to the liminal library.
245    ///
246    /// # Errors
247    /// Returns [`ServerError`] when the liminal conversation open operation fails.
248    fn open_conversation(
249        &self,
250        conversation_id: u64,
251        subject: &str,
252    ) -> Result<ConnectionConversation, ServerError>;
253
254    /// Delegates a conversation message to the liminal library.
255    ///
256    /// # Errors
257    /// Returns [`ServerError`] when the liminal conversation message operation fails.
258    fn conversation_message(
259        &self,
260        conversation: &ConnectionConversation,
261        envelope: &MessageEnvelope,
262    ) -> Result<(), ServerError>;
263
264    /// Delegates conversation close to the liminal library.
265    ///
266    /// # Errors
267    /// Returns [`ServerError`] when the liminal conversation close operation fails.
268    fn close_conversation(&self, conversation: ConnectionConversation) -> Result<(), ServerError>;
269
270    /// Flushes durable channel state through the liminal library boundary.
271    ///
272    /// # Errors
273    /// Returns [`ServerError`] when the liminal channel flush operation fails.
274    fn flush_durable_state(&self) -> Result<(), ServerError>;
275
276    /// Whether this adapter backs ordinary channel and conversation operations.
277    ///
278    /// The default is `true` — the full-service adapter serves publish, subscribe,
279    /// and conversation frames, so full mode is byte-for-byte unchanged. The
280    /// capability-scoped worker front door overrides this to `false`, letting
281    /// [`super::apply`] reject the channel/conversation frames it short-circuits on
282    /// empty connection state (`Unsubscribe`, `ConversationMessage`,
283    /// `ConversationClose`) with a typed error frame instead of silently swallowing
284    /// an operation for a resource that could never have been created in this
285    /// profile. Frames that always reach a service method (`Publish`, `Subscribe`,
286    /// `ConversationOpen`) are rejected by the front door's own method bodies and do
287    /// not consult this flag.
288    fn supports_channel_operations(&self) -> bool {
289        true
290    }
291
292    /// Whether `channel` admits `operation` right now.
293    ///
294    /// Consulted by the connection process BEFORE the operation is delegated, so
295    /// a roster refusal is typed at the moment of the decision, by the component
296    /// that made it, from the value it decided on. The alternative — classifying
297    /// an opaque failure afterwards by re-reading the roster in the error arm —
298    /// cannot tell "the roster refused this" from "something else failed while
299    /// the roster happened to change", and would put a confident wrong cause on
300    /// the wire.
301    ///
302    /// It does NOT replace the adapter's own inner check. Admission here is the
303    /// caller's guard; a service method is public and callable without a frame,
304    /// so it keeps its own.
305    ///
306    /// The default ADMITS. An adapter with no roster has nothing to say here and
307    /// its refusals travel as service errors exactly as they do today; only the
308    /// roster-owning adapter overrides this. A default body is also what keeps
309    /// this addition inside "minor": a method added without one breaks every
310    /// downstream implementor of a public trait.
311    ///
312    /// # Errors
313    /// Returns [`ChannelAccessError`] when the roster refuses the operation.
314    fn admit_channel(
315        &self,
316        operation: ChannelOperation,
317        channel: &str,
318    ) -> Result<(), ChannelAccessError> {
319        let _ = (operation, channel);
320        Ok(())
321    }
322}
323
324/// Default adapter from server wire frames to liminal channel/conversation APIs.
325#[derive(Debug)]
326pub struct LiminalConnectionServices {
327    /// The channel roster, keyed by channel name.
328    ///
329    /// Behind an [`RwLock`] over `Arc` values so a reader clones one pointer out
330    /// under the guard and works with it after the guard is released: no roster
331    /// read is ever held across a library call, and an entry handed out can
332    /// outlive a concurrent mutation of the map itself.
333    channels: RwLock<HashMap<String, Arc<ConfiguredChannel>>>,
334    /// The operator-declared bound on RUNTIME-registered channels
335    /// (`limits.max_channels`), carried verbatim from config.
336    ///
337    /// `None` is "the operator declared no bound", which refuses every runtime
338    /// registration rather than admitting an unbounded one — the roster's
339    /// aggregate size is otherwise unbounded the moment a registration API
340    /// exists. Boot-configured channels are never counted against it: they are
341    /// the bound the operator already wrote, in the file they wrote it in.
342    max_channels: Option<usize>,
343    cluster: ChannelCluster,
344    durable_store: Arc<dyn DurableStore>,
345    /// Complete participant service, installed only when semantic lifecycle
346    /// handling and its durable aggregate store are both ready.
347    participant_service: Option<InstalledParticipantService>,
348    /// The production handler's refused-load record, captured at construction.
349    ///
350    /// [`InstalledParticipantService`] holds its handler behind
351    /// `dyn ParticipantSemanticHandler`, so the concrete production handler is
352    /// no longer reachable once it is installed; the record is taken here, in
353    /// the one place the concrete handler exists, and carried out to the
354    /// startup path that publishes it onto the health endpoint. `None` is "no
355    /// participant is configured", which the operator surface reports as such
356    /// rather than as an empty refusal set.
357    unloadable_conversations: Option<UnloadableConversationRecord>,
358    /// The production handler as the A7 operator credential-re-issue authority
359    /// (§0.18), captured at construction for exactly the reason above: the
360    /// concrete handler is unreachable once erased behind
361    /// `dyn ParticipantSemanticHandler`, so the operator surface's handle on it
362    /// has to be taken in the one place it still exists. `None` is "no
363    /// participant is configured", which the route reports as such.
364    credential_reissuer: Option<Arc<dyn OperatorCredentialReissuer>>,
365    /// In-memory (haematite-backed) dedup cache for dedup-on-delivery. Keyed by
366    /// the per-message idempotency key carried on the publish frame; a duplicate
367    /// key is suppressed before fan-out so a subscriber receives it at most once.
368    /// Not persisted across restarts (13-L1 scope; durable dedup is deferred).
369    dedup: DedupCache,
370    conversation_supervisor: Arc<ConversationSupervisor>,
371    /// Registered custom conversation responders, keyed by conversation subject.
372    ///
373    /// When a conversation is opened (`open_conversation`), the subject is looked
374    /// up here: a registered [`ParticipantBehaviour`] becomes the conversation's
375    /// participant; with no registration the conversation falls back to the
376    /// built-in [`EchoBehaviour`], preserving the original echo semantics exactly.
377    /// This is the seam aion #13 plugs a remote worker responder into. Interior
378    /// mutability is required because the services are shared behind `&self`.
379    responders: Mutex<ResponderRegistry>,
380    next_message_id: AtomicU64,
381    next_subscription_id: AtomicU64,
382}
383
384impl LiminalConnectionServices {
385    /// Builds library-backed services from validated server configuration.
386    ///
387    /// Durable-mode channels are backed by a shared haematite event store so
388    /// their publishes are persisted and survive the graceful-shutdown flush;
389    /// ephemeral channels carry no store.
390    ///
391    /// Full-only: a config selecting the worker-front-door profile is rejected at
392    /// entry (before any store is built), so this constructor can never build full
393    /// services for a profile that forbids them. Profile-aware callers go through
394    /// [`build_connection_services`] instead.
395    ///
396    /// # Errors
397    /// Returns [`ServerError`] when the config selects a non-full profile or a
398    /// configured channel cannot be initialized.
399    pub fn from_config(config: &ServerConfig) -> Result<Self, ServerError> {
400        require_full_profile(config)?;
401        let store = ProductionSubsystems.durable_store(config.persistence_path.as_deref())?;
402        Self::from_config_with_store_via(config, store, &ProductionSubsystems)
403    }
404
405    /// Builds services over a caller-provided durable store.
406    ///
407    /// Used by tests that need to inspect persisted state through the same store
408    /// handle the durable channels write to.
409    ///
410    /// Full-only: rejects a worker-front-door profile at entry, exactly like
411    /// [`Self::from_config`].
412    ///
413    /// # Errors
414    /// Returns [`ServerError`] when the config selects a non-full profile or a
415    /// configured channel cannot be initialized.
416    pub fn from_config_with_store(
417        config: &ServerConfig,
418        durable_store: Arc<dyn DurableStore>,
419    ) -> Result<Self, ServerError> {
420        require_full_profile(config)?;
421        Self::from_config_with_store_via(config, durable_store, &ProductionSubsystems)
422    }
423
424    /// [`Self::from_config_with_store`] with the subsystem factory injected.
425    ///
426    /// The channel supervisor and conversation supervisor are constructed ONLY
427    /// through `subsystems` — there is no direct constructor call in this body —
428    /// so a factory that records as a side effect of constructing cannot have its
429    /// recording omitted (§9 D2 seam census, record-by-construction). No profile
430    /// check here: the caller (the public wrapper or the profile dispatch in
431    /// [`build_connection_services`]) has already established the full profile.
432    fn from_config_with_store_via(
433        config: &ServerConfig,
434        durable_store: Arc<dyn DurableStore>,
435        subsystems: &dyn SubsystemFactory,
436    ) -> Result<Self, ServerError> {
437        // Build ONE shared channel supervisor for the whole server. When a
438        // [cluster] section is present it is distribution-enabled, so every
439        // channel actor and subscriber shares the clustered scheduler the cluster
440        // attaches its process-group transport to (SRV-005, Constraint B).
441        let cluster = subsystems.channel_cluster(config.cluster.as_ref())?;
442        let mut channels = HashMap::new();
443        for channel in &config.channels {
444            // Resolve the channel's real JSON Schema (loaded from `schema_ref`
445            // during config validation) or the permissive empty schema when the
446            // channel declared none. The protocol schema id advertised at
447            // subscribe time is derived from the SAME schema bytes so an SDK
448            // deriving ids from schema bytes converges on it.
449            let resolved = resolve_channel_schema(channel);
450            let configured = build_configured_channel(
451                &channel.name,
452                resolved,
453                channel.durable,
454                ChannelOrigin::BootConfigured,
455                &durable_store,
456                cluster.supervisor(),
457            )
458            .map_err(|error| ServerError::ConfigValidation {
459                message: error.boot_message(&channel.name),
460            })?;
461            channels.insert(channel.name.clone(), Arc::new(configured));
462        }
463        let conversation_supervisor = subsystems.conversation_supervisor()?;
464        let dedup = DedupCache::new(Arc::clone(&durable_store), DELIVERY_DEDUP_NAMESPACE);
465        // Production participant activation (LP gap closure, Part B): the
466        // deployment's [participant] section installs the ONE production
467        // semantic handler, sealed together with the same durable store the
468        // conversation logs live in, under the configured wire-frame limit.
469        // No section, no service — the capability bit stays off and the
470        // connection path is byte-identical to the pre-activation build.
471        let installed_participant = config
472            .participant
473            .as_ref()
474            .map(|participant| {
475                let handler =
476                    ProductionParticipantHandler::new(Arc::clone(&durable_store), *participant)
477                        .map_err(|error| ServerError::ParticipantStartupRestore {
478                            message: error.to_string(),
479                        })?;
480                // Taken BEFORE the handler is erased behind
481                // `dyn ParticipantSemanticHandler`: boot has already recorded
482                // every conversation it refused by the time `new` returns, so
483                // the record captured here is complete from the first scrape.
484                let unloadable_conversations = handler.unloadable_record();
485                let handler = Arc::new(handler);
486                // A7 (§0.18): taken here for the same reason, and from the same
487                // one live handler, so the operator surface and the participant
488                // wire cannot end up talking to two different owners.
489                let credential_reissuer: Arc<dyn OperatorCredentialReissuer> =
490                    Arc::<ProductionParticipantHandler>::clone(&handler);
491                let service = InstalledParticipantService::new(
492                    handler,
493                    Arc::clone(&durable_store),
494                    participant.wire_frame_limit,
495                )
496                .map_err(|error| ServerError::ConfigValidation {
497                    message: format!(
498                        "participant.wire_frame_limit: {} is below the protocol's minimum \
499                         complete participant frame ({error:?})",
500                        participant.wire_frame_limit
501                    ),
502                })?;
503                Ok::<_, ServerError>((
504                    service,
505                    (unloadable_conversations, credential_reissuer),
506                ))
507            })
508            .transpose()?;
509        let (participant_service, operator_surfaces) = installed_participant.unzip();
510        let (unloadable_conversations, credential_reissuer) = operator_surfaces.unzip();
511        Ok(Self {
512            channels: RwLock::new(channels),
513            max_channels: config.limits.max_channels,
514            cluster,
515            durable_store,
516            participant_service,
517            unloadable_conversations,
518            credential_reissuer,
519            dedup,
520            conversation_supervisor,
521            responders: Mutex::new(HashMap::new()),
522            next_message_id: AtomicU64::new(1),
523            next_subscription_id: AtomicU64::new(1),
524        })
525    }
526
527    /// Builds services with no configured channels.
528    ///
529    /// # Errors
530    /// Returns [`ServerError`] when the conversation supervisor scheduler cannot start.
531    pub fn empty() -> Result<Self, ServerError> {
532        let conversation_supervisor = ProductionSubsystems.conversation_supervisor()?;
533        let durable_store = build_durable_store(None)?;
534        let dedup = DedupCache::new(Arc::clone(&durable_store), DELIVERY_DEDUP_NAMESPACE);
535        Ok(Self {
536            channels: RwLock::new(HashMap::new()),
537            // No config, so no declared bound: this builder serves tests and
538            // callers with no channels at all, and a registration against it
539            // refuses `CapNotConfigured` exactly as an undeclared deployment's
540            // would.
541            max_channels: None,
542            cluster: build_channel_cluster(None)?,
543            durable_store,
544            participant_service: None,
545            unloadable_conversations: None,
546            credential_reissuer: None,
547            dedup,
548            conversation_supervisor,
549            responders: Mutex::new(HashMap::new()),
550            next_message_id: AtomicU64::new(1),
551            next_subscription_id: AtomicU64::new(1),
552        })
553    }
554
555    /// The shared channel supervisor + cluster resolver backing this service.
556    ///
557    /// The server runtime uses this to attach the cluster to the channel
558    /// supervisor's clustered scheduler (SRV-005).
559    #[must_use]
560    pub const fn channel_cluster(&self) -> &ChannelCluster {
561        &self.cluster
562    }
563
564    /// Returns the shared durable store backing this service's durable channels.
565    #[must_use]
566    pub fn durable_store(&self) -> Arc<dyn DurableStore> {
567        Arc::clone(&self.durable_store)
568    }
569
570    /// The production participant handler's refused-load record, when a
571    /// participant is configured.
572    ///
573    /// The server's startup path publishes this onto the health endpoint so
574    /// `GET /unloadable-conversations` answers from the same record the
575    /// handler writes. `None` means no participant is configured at all, which
576    /// the surface reports as a distinct state from "nothing was refused".
577    #[must_use]
578    pub fn unloadable_conversation_record(&self) -> Option<UnloadableConversationRecord> {
579        self.unloadable_conversations.clone()
580    }
581
582    /// The A7 operator credential-re-issue authority, when a participant is
583    /// configured (§0.18).
584    ///
585    /// The startup path publishes this onto the health endpoint so
586    /// `POST /operator/credential-reissue` reaches the SAME serialized
587    /// participant-state point every wire operation does. `None` means no
588    /// participant is configured, which the route reports as a distinct state
589    /// from an unknown identity.
590    #[must_use]
591    pub fn credential_reissuer(&self) -> Option<Arc<dyn OperatorCredentialReissuer>> {
592        self.credential_reissuer.clone()
593    }
594
595    /// Installs a complete participant bundle in full-service supervisor tests.
596    ///
597    /// Production full services intentionally stay disabled until a concrete
598    /// lifecycle handler exists. This consuming test builder exercises the real
599    /// supervisor activation path without allowing an already-shared adapter to
600    /// change capability posture.
601    #[cfg(test)]
602    #[must_use]
603    pub(crate) fn with_participant_service(
604        mut self,
605        participant_service: InstalledParticipantService,
606    ) -> Self {
607        self.participant_service = Some(participant_service);
608        self
609    }
610
611    /// Returns the conversation supervisor backing supervised conversations.
612    ///
613    /// Tests use this to reach the underlying beamr scheduler so they can spawn
614    /// or terminate participant processes and exercise crash detection.
615    #[must_use]
616    pub fn conversation_supervisor(&self) -> Arc<ConversationSupervisor> {
617        Arc::clone(&self.conversation_supervisor)
618    }
619
620    /// Registers a custom conversation responder for a routing `subject`.
621    ///
622    /// When a conversation is later opened with this exact `subject`, its
623    /// participant runs `behaviour` instead of the built-in [`EchoBehaviour`].
624    /// The responder is spawned and supervised identically to the echo
625    /// participant — a real linked beamr process with the same crash-detection
626    /// semantics — so this exposes the responder seam without changing how
627    /// participants run. Registering a subject that already has a responder
628    /// replaces it; the previous behaviour is returned.
629    ///
630    /// This is the liminal-side seam aion #13 plugs a remote worker into: it
631    /// registers a responder that forwards each request to the worker and routes
632    /// the worker's reply back through the conversation. Subjects with no
633    /// registration keep echoing, so existing callers are unaffected.
634    ///
635    /// # Errors
636    /// Returns [`ServerError`] when the responder registry lock is poisoned.
637    pub fn register_responder(
638        &self,
639        subject: impl Into<String>,
640        behaviour: Arc<dyn ParticipantBehaviour>,
641    ) -> Result<Option<Arc<dyn ParticipantBehaviour>>, ServerError> {
642        let mut responders = self.lock_responders()?;
643        Ok(responders.insert(subject.into(), behaviour))
644    }
645
646    /// Removes the custom responder registered for `subject`, if any.
647    ///
648    /// After removal the subject reverts to the built-in [`EchoBehaviour`] on the
649    /// next [`Self::open_conversation`]. Returns the removed behaviour when one
650    /// was registered.
651    ///
652    /// # Errors
653    /// Returns [`ServerError`] when the responder registry lock is poisoned.
654    pub fn unregister_responder(
655        &self,
656        subject: &str,
657    ) -> Result<Option<Arc<dyn ParticipantBehaviour>>, ServerError> {
658        let mut responders = self.lock_responders()?;
659        Ok(responders.remove(subject))
660    }
661
662    /// Resolves the responder behaviour for `subject`: the registered custom
663    /// responder when present, otherwise the built-in [`EchoBehaviour`].
664    ///
665    /// This is the single routing decision behind the seam — registered-or-echo —
666    /// so the fallback is identical to the original hard-wired echo path.
667    fn responder_for(&self, subject: &str) -> Result<Arc<dyn ParticipantBehaviour>, ServerError> {
668        let responders = self.lock_responders()?;
669        Ok(responders.get(subject).map_or_else(
670            || Arc::new(EchoBehaviour) as Arc<dyn ParticipantBehaviour>,
671            Arc::clone,
672        ))
673    }
674
675    /// Locks the responder registry, mapping a poisoned lock to a [`ServerError`]
676    /// rather than panicking (the workspace denies `unwrap`/`expect`/`panic`).
677    fn lock_responders(&self) -> Result<std::sync::MutexGuard<'_, ResponderRegistry>, ServerError> {
678        self.responders
679            .lock()
680            .map_err(|_poisoned| ServerError::ListenerAccept {
681                message: "responder registry lock poisoned".to_owned(),
682            })
683    }
684
685    /// Takes the channel roster's read guard, mapping a poisoned lock to a
686    /// [`ServerError`] rather than panicking (the workspace denies
687    /// `unwrap`/`expect`/`panic`), exactly as [`Self::lock_responders`] does for
688    /// the responder registry.
689    ///
690    /// Every caller clones the `Arc` it needs out of the returned guard and drops
691    /// the guard before doing anything else: the roster lock is never held across
692    /// a call into the liminal library.
693    fn read_channels(
694        &self,
695    ) -> Result<std::sync::RwLockReadGuard<'_, HashMap<String, Arc<ConfiguredChannel>>>, ServerError>
696    {
697        self.channels
698            .read()
699            .map_err(|_poisoned| ServerError::ListenerAccept {
700                message: "channel roster lock poisoned".to_owned(),
701            })
702    }
703
704    /// Subscribes to a configured channel and returns the raw library
705    /// subscription handle so a test can drain the subscriber inbox directly and
706    /// observe exactly which messages reached a subscriber.
707    #[cfg(test)]
708    pub(crate) fn subscribe_handle_for_test(
709        &self,
710        channel: &str,
711    ) -> Result<liminal::channel::SubscriptionHandle, ServerError> {
712        let channels = self.read_channels()?;
713        let configured =
714            channels
715                .get(channel)
716                .map(Arc::clone)
717                .ok_or_else(|| ServerError::ListenerAccept {
718                    message: format!("channel '{channel}' is not configured"),
719                })?;
720        drop(channels);
721        configured
722            .handle
723            .subscribe()
724            .map_err(|error| ServerError::ListenerAccept {
725                message: format!("liminal subscribe failed for channel '{channel}': {error}"),
726            })
727    }
728
729    /// Claims the delivery right for an idempotency key.
730    ///
731    /// Returns `Ok(true)` when this is the first publish for the key (the caller
732    /// may deliver), and `Ok(false)` when the key was already claimed/completed (a
733    /// duplicate the caller must suppress). The dedup cache is driven synchronously
734    /// over the in-memory haematite store via the durable bridge.
735    fn claim_delivery(&self, key: &str) -> Result<bool, ServerError> {
736        let decision = block_on(self.dedup.claim_or_get(key, dedup_timestamp_millis()))
737            .map_err(|error| ServerError::ListenerAccept {
738                message: format!("dedup bridge failed for key '{key}': {error}"),
739            })?
740            .map_err(|error| ServerError::ListenerAccept {
741                message: format!("dedup claim failed for key '{key}': {error}"),
742            })?;
743        Ok(matches!(decision, DedupDecision::Claimed))
744    }
745
746    /// Releases a dangling in-flight dedup claim after a failed delivery.
747    ///
748    /// Best-effort: a release failure cannot mask the original publish error, so
749    /// this returns nothing and logs at `error` level instead of surfacing. It is
750    /// never silent — the leak (a permanently suppressed key) must be observable.
751    /// `release_claim` itself never clobbers a stored receipt, so calling it on the
752    /// failure path is safe even if a concurrent completion raced ahead.
753    fn release_claim(&self, key: &str) {
754        match block_on(self.dedup.release_claim(key)) {
755            Ok(Ok(())) => {}
756            Ok(Err(error)) => {
757                tracing::error!(
758                    idempotency_key = key,
759                    %error,
760                    "failed to release dedup claim after publish failure; key may stay suppressed"
761                );
762            }
763            Err(error) => {
764                tracing::error!(
765                    idempotency_key = key,
766                    %error,
767                    "dedup release bridge failed after publish failure; key may stay suppressed"
768                );
769            }
770        }
771    }
772}
773
774/// The runtime channel-registration surface.
775///
776/// Inherent methods, not trait methods, and deliberately so: this is
777/// authority-moving vocabulary that belongs to the ONE adapter owning a channel
778/// roster. Putting `register`/`quiesce` on the public [`ConnectionServices`]
779/// trait would break every external implementor and would hand
780/// register/quiesce words to a profile that serves no channels at all.
781///
782/// The same seam as the existing runtime-mutation API on this type
783/// (`register_responder`/`unregister_responder`): `&self`, interior mutability,
784/// typed `Result`.
785impl LiminalConnectionServices {
786    /// Registers `spec` on the live roster.
787    ///
788    /// Idempotent when an entry of that name already has an IDENTICAL
789    /// configuration — mode, protocol schema id, and schema document, all three
790    /// — and refuses typed, naming the first differing field, otherwise. An
791    /// identical registration against a boot-configured entry answers
792    /// [`Registered::AlreadyIdentical`] and leaves its origin alone: flipping it
793    /// would make the entry lie about its restart fate and would move it into
794    /// the counted population without a channel having been created.
795    ///
796    /// The cap is consulted first, in two steps with different reach. An absent
797    /// `limits.max_channels` refuses EVERY call, identical or not, before the
798    /// roster is read at all: a deployment that has declared no bound has not
799    /// said what it admits, and unbounded-by-default is not a bound. A REACHED
800    /// cap refuses only a call that would create an entry — it gates the insert,
801    /// which is the population it bounds. An identical re-registration inserts
802    /// nothing and so is answered `AlreadyIdentical` even at a full roster;
803    /// refusing it would break idempotency at exactly the boundary a projector
804    /// re-projecting its record crosses.
805    ///
806    /// # Errors
807    /// Returns [`ChannelRegistryError`] when no cap is configured, the cap is
808    /// reached, the name exists with a different configuration, the schema bytes
809    /// do not parse or compile, durable initialization over the shared store
810    /// fails, or the roster lock is poisoned.
811    pub fn register_channel(
812        &self,
813        spec: &ChannelRegistration,
814    ) -> Result<Registered, ChannelRegistryError> {
815        // The cap is consulted before the roster is touched: an undeclared bound
816        // refuses every runtime registration, identical or not, because the
817        // deployment has not said what it admits.
818        let Some(limit) = self.max_channels else {
819            return Err(ChannelRegistryError::CapNotConfigured {
820                cap: MAX_CHANNELS_KEY,
821            });
822        };
823        // Schema resolution is pure (a JSON parse and a digest), so it runs with
824        // no lock held and its result serves BOTH the identity comparison and
825        // the construction below.
826        let resolved = resolve_schema_bytes(spec.schema_bytes.as_deref()).map_err(|error| {
827            ChannelRegistryError::SchemaRejected {
828                name: spec.name.clone(),
829                message: error.to_string(),
830            }
831        })?;
832
833        // Fast paths, under a READ lock: an already-identical registration
834        // builds nothing, and a full roster refuses before paying for a
835        // construction it would discard. Neither is the authority — the write
836        // lock below re-decides both, because the roster can move in between.
837        let (existing, registered_count) = {
838            let channels = self.read_roster()?;
839            let existing = channels.get(&spec.name).map(Arc::clone);
840            let registered_count = runtime_registered_count(&channels);
841            drop(channels);
842            (existing, registered_count)
843        };
844        if let Some(existing) = existing {
845            return compare_registration(&existing, spec, &resolved);
846        }
847        if registered_count >= limit {
848            return Err(ChannelRegistryError::CapReached {
849                cap: MAX_CHANNELS_KEY,
850                limit,
851            });
852        }
853
854        // Construction runs with NO lock held. A durable channel recovers its
855        // per-partition sequence counters from the store here, which is O(stream
856        // length) in store reads; holding the roster lock across it would put a
857        // slow store walk on every connection's publish and subscribe path.
858        let configured = build_configured_channel(
859            &spec.name,
860            resolved,
861            spec.durable,
862            ChannelOrigin::RuntimeRegistered,
863            &self.durable_store,
864            self.cluster.supervisor(),
865        )
866        .map_err(|error| error.into_registry_error(&spec.name))?;
867
868        // The authoritative decision: identity, cap, and insert under ONE write
869        // lock, so the count that admitted the entry and the insert that added
870        // it cannot be separated by a concurrent registration.
871        let mut channels = self.write_roster()?;
872        let raced = channels.get(&spec.name).map(Arc::clone);
873        let registered_count = runtime_registered_count(&channels);
874        if raced.is_none() && registered_count < limit {
875            channels.insert(spec.name.clone(), Arc::new(configured));
876            drop(channels);
877            return Ok(Registered::Created);
878        }
879        drop(channels);
880        if let Some(raced) = raced {
881            // A racer registered this name while the channel above was being
882            // built. The built entry is dropped unused: it owns no actor (the
883            // actor is spawned lazily on first use) and its durable
884            // construction only READ the store, so discarding it changes
885            // nothing an observer could see.
886            return compare_registration(&raced, spec, &schema_of(&configured));
887        }
888        Err(ChannelRegistryError::CapReached {
889            cap: MAX_CHANNELS_KEY,
890            limit,
891        })
892    }
893
894    /// Moves `name` from active to quiesced with a named `reason`. ONE-WAY.
895    ///
896    /// New publishes and new subscribes are refused afterwards, carrying the
897    /// reason. Existing subscriptions are UNTOUCHED: nothing revokes a
898    /// subscription handle, the actor's subscriber list is not walked, no EXIT
899    /// is sent, and the channel actor keeps running. Quiesce is a roster-level
900    /// admission decision, not an actor command.
901    ///
902    /// Re-quiescing under the IDENTICAL reason is `Ok(())`; a DIFFERENT reason
903    /// refuses, carrying the reason already on record.
904    ///
905    /// The return does NOT mean "no new subscriber can appear". A subscribe that
906    /// has already passed admission completes and gets its stream — the
907    /// linearisation point is the admission read, not the subscribe's
908    /// completion. A consumer that needs "nobody is attached" must observe
909    /// attachment directly.
910    ///
911    /// # Errors
912    /// Returns [`ChannelRegistryError`] when the name is not registered, is
913    /// already quiesced under a different reason, or the roster lock is
914    /// poisoned.
915    pub fn quiesce_channel(
916        &self,
917        name: &str,
918        reason: impl Into<String>,
919    ) -> Result<(), ChannelRegistryError> {
920        let reason = reason.into();
921        // A READ lock: the state machine lives on the ENTRY, not in the map, so
922        // the operation the design most wants to be safe never blocks a reader.
923        let configured = {
924            let channels = self.read_roster()?;
925            channels.get(name).map(Arc::clone).ok_or_else(|| {
926                ChannelRegistryError::NotRegistered {
927                    name: name.to_owned(),
928                }
929            })?
930        };
931        configured.quiesce(name, &reason)
932    }
933
934    /// Cheap typed probe: one roster read plus one atomic load.
935    ///
936    /// Touches no actor and therefore CANNOT spawn one. Every handle accessor
937    /// that could answer a question about a channel's activity routes through
938    /// the lazy-spawn path, so a probe built on one would materialise the actor
939    /// of the idle channel it was asked about — turning a read into a side
940    /// effect. This reads the roster entry's own recorded fields and nothing
941    /// else.
942    ///
943    /// # Errors
944    /// Returns [`ChannelRegistryError::RosterUnavailable`] only.
945    pub fn channel_status(&self, name: &str) -> Result<ChannelStatus, ChannelRegistryError> {
946        let configured = {
947            let channels = self.read_roster()?;
948            channels.get(name).map(Arc::clone)
949        };
950        let Some(configured) = configured else {
951            return Ok(ChannelStatus::NotRegistered);
952        };
953        let mode = configured.handle.config().mode;
954        Ok(match configured.state() {
955            ChannelState::Active => ChannelStatus::Active {
956                origin: configured.origin,
957                mode,
958                schema: configured.protocol_schema,
959            },
960            ChannelState::Quiesced { reason } => ChannelStatus::Quiesced {
961                reason,
962                origin: configured.origin,
963                mode,
964            },
965        })
966    }
967
968    /// The whole roster: one minimal descriptor per entry, sorted by name.
969    ///
970    /// The census companion to [`Self::channel_status`]. A by-name probe answers
971    /// about a name the caller already suspects; only an enumeration can reveal
972    /// a name the caller does not know to ask about, and a verification sweep
973    /// with no population denominator is an instrument shape this estate
974    /// forbids. Touches no actor, under the same constraint as the probe.
975    ///
976    /// # Errors
977    /// Returns [`ChannelRegistryError::RosterUnavailable`] only.
978    pub fn registered_channels(&self) -> Result<Vec<ChannelDescriptor>, ChannelRegistryError> {
979        let entries: Vec<(String, Arc<ConfiguredChannel>)> = {
980            let channels = self.read_roster()?;
981            channels
982                .iter()
983                .map(|(name, configured)| (name.clone(), Arc::clone(configured)))
984                .collect()
985        };
986        let mut descriptors: Vec<ChannelDescriptor> = entries
987            .into_iter()
988            .map(|(name, configured)| ChannelDescriptor {
989                name,
990                origin: configured.origin,
991                state: configured.state(),
992            })
993            .collect();
994        descriptors.sort_by(|left, right| left.name.cmp(&right.name));
995        Ok(descriptors)
996    }
997
998    /// The roster admission funnel: the ONE place a channel operation's
999    /// permission is decided.
1000    ///
1001    /// Returns the admitted entry, so the caller works from the value the
1002    /// decision was made on rather than reading the roster a second time and
1003    /// risking a different answer. This read is the LINEARISATION POINT for the
1004    /// quiesce race: everything the caller does with the returned entry happens
1005    /// outside the lock and is not re-checked, which is exactly why a quiesce
1006    /// that commits after this returns does not stop the operation it admitted.
1007    ///
1008    /// # Errors
1009    /// Returns [`ChannelAccessError`] when the channel is absent, quiesced, or
1010    /// the roster lock is poisoned.
1011    fn admit_channel(&self, channel: &str) -> Result<Arc<ConfiguredChannel>, ChannelAccessError> {
1012        let configured = {
1013            let channels = self.channels.read().map_err(|_poisoned| {
1014                ChannelAccessError::RosterUnavailable {
1015                    message: ROSTER_POISONED.to_owned(),
1016                }
1017            })?;
1018            channels.get(channel).map(Arc::clone)
1019        };
1020        let configured = configured.ok_or_else(|| ChannelAccessError::NotRegistered {
1021            name: channel.to_owned(),
1022        })?;
1023        if configured.state.load(Ordering::Acquire) == STATE_QUIESCED {
1024            return Err(ChannelAccessError::Quiesced {
1025                name: channel.to_owned(),
1026                reason: configured.recorded_quiesce_reason(),
1027            });
1028        }
1029        Ok(configured)
1030    }
1031
1032    /// Takes the roster's READ guard for the registration surface, mapping a
1033    /// poisoned lock to a typed [`ChannelRegistryError`].
1034    ///
1035    /// Separate from [`Self::read_channels`] because the two surfaces answer
1036    /// different callers with different error types; recovering one from the
1037    /// other would mean inspecting a message.
1038    fn read_roster(
1039        &self,
1040    ) -> Result<
1041        std::sync::RwLockReadGuard<'_, HashMap<String, Arc<ConfiguredChannel>>>,
1042        ChannelRegistryError,
1043    > {
1044        self.channels
1045            .read()
1046            .map_err(|_poisoned| ChannelRegistryError::RosterUnavailable {
1047                message: ROSTER_POISONED.to_owned(),
1048            })
1049    }
1050
1051    /// Takes the roster's WRITE guard — held only for the check-and-insert that
1052    /// must be atomic, never across a call into the liminal library.
1053    fn write_roster(
1054        &self,
1055    ) -> Result<
1056        std::sync::RwLockWriteGuard<'_, HashMap<String, Arc<ConfiguredChannel>>>,
1057        ChannelRegistryError,
1058    > {
1059        self.channels
1060            .write()
1061            .map_err(|_poisoned| ChannelRegistryError::RosterUnavailable {
1062                message: ROSTER_POISONED.to_owned(),
1063            })
1064    }
1065}
1066
1067/// The diagnostic carried when the roster lock is poisoned. One string, shared
1068/// by every surface that reports it, so the wording cannot drift between them.
1069const ROSTER_POISONED: &str = "channel roster lock poisoned";
1070
1071/// How many roster entries the registration cap counts.
1072///
1073/// Runtime-registered entries ONLY. Boot-configured channels are the operator's
1074/// own authored bound — they are in the file the operator wrote — and counting
1075/// them would make one number mean two different things depending on how the
1076/// deployment was configured. The origin never flips, so this population is
1077/// well defined over time.
1078fn runtime_registered_count(channels: &HashMap<String, Arc<ConfiguredChannel>>) -> usize {
1079    channels
1080        .values()
1081        .filter(|configured| configured.origin == ChannelOrigin::RuntimeRegistered)
1082        .count()
1083}
1084
1085/// The three-field identity comparison behind idempotent-if-identical
1086/// registration.
1087///
1088/// All three must match; the FIRST mismatch is reported by name. The name itself
1089/// is not compared — it is the roster key, a precondition of the comparison
1090/// rather than a member of it — and neither are the supervisor or the durable
1091/// store, which are one server-wide instance each and cannot differ between two
1092/// registrations in one process.
1093///
1094/// The schema is compared as BOTH its protocol id and its parsed document, on
1095/// purpose. The id is a 64-bit non-cryptographic digest, so the document guards
1096/// against a collision accepting a different schema as identical; and the two
1097/// fail in opposite directions, because two byte sequences that parse to the
1098/// same document but differ in whitespace produce different ids — which must
1099/// refuse, since the id is what every future subscriber negotiates.
1100///
1101/// The channel's own `Schema` is not compared: it is not `PartialEq`, and its
1102/// identifier is a fresh value per construction, so comparing it would refuse
1103/// every idempotent re-registration.
1104fn compare_registration(
1105    existing: &ConfiguredChannel,
1106    spec: &ChannelRegistration,
1107    resolved: &ChannelSchema,
1108) -> Result<Registered, ChannelRegistryError> {
1109    let requested_mode = if spec.durable {
1110        ChannelMode::Durable
1111    } else {
1112        ChannelMode::Ephemeral
1113    };
1114    let config = existing.handle.config();
1115    let mismatch = if config.mode == requested_mode {
1116        if existing.protocol_schema == resolved.protocol_id {
1117            if *config.schema.definition() == resolved.document {
1118                None
1119            } else {
1120                Some(ChannelConfigField::SchemaDocument)
1121            }
1122        } else {
1123            Some(ChannelConfigField::SchemaId)
1124        }
1125    } else {
1126        Some(ChannelConfigField::Mode)
1127    };
1128    mismatch.map_or(Ok(Registered::AlreadyIdentical), |field| {
1129        Err(ChannelRegistryError::AlreadyRegistered {
1130            name: spec.name.clone(),
1131            field,
1132        })
1133    })
1134}
1135
1136/// Recovers the resolved schema of an already-built entry, so the write-lock
1137/// re-check compares the SAME three fields against the same values the fast path
1138/// would have.
1139fn schema_of(configured: &ConfiguredChannel) -> ChannelSchema {
1140    ChannelSchema {
1141        document: configured.handle.config().schema.definition().clone(),
1142        protocol_id: configured.protocol_schema,
1143    }
1144}
1145
1146/// Builds one roster entry: the channel's validation engine, its library handle
1147/// (durable or ephemeral), and the protocol schema id advertised at subscribe
1148/// time.
1149///
1150/// This is the SOLE place a [`ConfiguredChannel`] is constructed. Keeping
1151/// construction in one function is what makes "every channel on this server is
1152/// the same kind of object" structural rather than a promise: there is no second
1153/// body a channel could be built by, so no channel can drift into a different
1154/// shape. The boot loop in [`LiminalConnectionServices::from_config_with_store_via`]
1155/// is its caller.
1156///
1157/// `resolved` is consumed because its JSON Schema document is moved into the
1158/// channel's [`Schema`]; the protocol id is carried onto the entry unchanged.
1159/// `origin` is stamped here and never written again.
1160///
1161/// The failure is typed ([`ChannelBuildError`]) rather than a [`ServerError`] so
1162/// BOTH callers can render it without inspecting a message: the boot loop maps
1163/// it back to the exact `ConfigValidation` strings it has always produced, and
1164/// `register_channel` maps it to the registry's own typed variants.
1165///
1166/// # Errors
1167/// Returns [`ChannelBuildError`] when the JSON Schema document does not compile
1168/// or durable initialization over the shared store fails.
1169fn build_configured_channel(
1170    name: &str,
1171    resolved: ChannelSchema,
1172    durable: bool,
1173    origin: ChannelOrigin,
1174    durable_store: &Arc<dyn DurableStore>,
1175    supervisor: &ChannelSupervisor,
1176) -> Result<ConfiguredChannel, ChannelBuildError> {
1177    let schema =
1178        Schema::new(resolved.document).map_err(|error| ChannelBuildError::SchemaRejected {
1179            message: error.to_string(),
1180        })?;
1181    let channel_config = if durable {
1182        ChannelConfig::new(name.to_owned(), schema, ChannelMode::Durable)
1183    } else {
1184        ChannelConfig::new(name.to_owned(), schema, ChannelMode::Ephemeral)
1185    };
1186    let handle = if durable {
1187        ChannelHandle::new_durable_with_supervisor(
1188            channel_config,
1189            Arc::clone(durable_store),
1190            supervisor.clone(),
1191        )
1192        .map_err(|error| ChannelBuildError::DurableInitFailed {
1193            message: error.to_string(),
1194        })?
1195    } else {
1196        ChannelHandle::with_supervisor(channel_config, supervisor.clone())
1197    };
1198    Ok(ConfiguredChannel {
1199        handle,
1200        protocol_schema: resolved.protocol_id,
1201        origin,
1202        state: AtomicU8::new(STATE_ACTIVE),
1203        quiesce_reason: OnceLock::new(),
1204    })
1205}
1206
1207/// Returns the current epoch-millis timestamp used as the dedup entry anchor.
1208///
1209/// A clock error before the Unix epoch yields `0`: the timestamp is only a TTL
1210/// anchor for the in-memory cache and a zero anchor never breaks the at-most-once
1211/// claim semantics, so this avoids surfacing a clock fault on the publish path.
1212fn dedup_timestamp_millis() -> u64 {
1213    use std::time::{SystemTime, UNIX_EPOCH};
1214    SystemTime::now()
1215        .duration_since(UNIX_EPOCH)
1216        .ok()
1217        .and_then(|duration| u64::try_from(duration.as_millis()).ok())
1218        .unwrap_or(0)
1219}
1220
1221/// Default shard count for an on-disk durable store.
1222///
1223/// Haematite routes keys across this many single-threaded shard actors; a small
1224/// power of two gives parallelism across cursors/streams without spawning an
1225/// actor per core. The value is fixed (haematite has no silent default) and not
1226/// yet surfaced in server config.
1227const DEFAULT_SHARD_COUNT: usize = 8;
1228
1229/// Namespace prefix for the dedup-on-delivery cache streams. Keeps delivery dedup
1230/// keys from colliding with any other haematite streams in the shared store.
1231const DELIVERY_DEDUP_NAMESPACE: &str = "liminal:delivery-dedup";
1232
1233/// Beamr-scheduler-owning subsystems the full-service construction path builds
1234/// beyond the connection supervisor's own scheduler. The §9 D2 seam census counts
1235/// these: the worker-front-door profile must construct NONE of them. Test-gated:
1236/// this is the recording vocabulary of the gate's instrument, not production state.
1237#[cfg(test)]
1238#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1239pub(super) enum SchedulerSubsystem {
1240    /// The shared channel supervisor (its own beamr scheduler).
1241    ChannelSupervisor,
1242    /// The conversation supervisor (its own beamr scheduler).
1243    ConversationSupervisor,
1244    /// The haematite store's database (its shard-actor scheduler).
1245    HaematiteStore,
1246}
1247
1248/// Constructor seam for every scheduler-owning subsystem (`SchedulerSubsystem`)
1249/// the profile-aware construction path can create.
1250///
1251/// These methods are the ONLY route through which [`build_connection_services`]
1252/// and the [`LiminalConnectionServices`] config constructors reach
1253/// `build_channel_cluster`, `ConversationSupervisor::new`, and the durable-store
1254/// constructors — no direct constructor call exists in those bodies. The §9 D2
1255/// gate therefore injects a factory that records as a side effect of
1256/// constructing (the D3 store-seam ownership move applied to schedulers): a
1257/// recording cannot be omitted without also failing to construct the subsystem,
1258/// which closes the "hand-placed census call beside the constructor" gap where a
1259/// future subsystem could be built without its courtesy call.
1260pub(super) trait SubsystemFactory {
1261    /// Constructs the shared channel supervisor + cluster resolver (a beamr
1262    /// scheduler).
1263    ///
1264    /// # Errors
1265    /// Returns [`ServerError`] when the channel supervisor scheduler cannot start.
1266    fn channel_cluster(
1267        &self,
1268        cluster_config: Option<&ClusterConfig>,
1269    ) -> Result<ChannelCluster, ServerError>;
1270
1271    /// Constructs the conversation supervisor (a beamr scheduler).
1272    ///
1273    /// # Errors
1274    /// Returns [`ServerError`] when the conversation supervisor scheduler cannot
1275    /// start.
1276    fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError>;
1277
1278    /// Constructs the durable store (haematite's shard-actor scheduler):
1279    /// persistent under `persistence_path`, self-owning ephemeral otherwise.
1280    ///
1281    /// # Errors
1282    /// Returns [`ServerError`] when the store cannot be opened.
1283    fn durable_store(
1284        &self,
1285        persistence_path: Option<&Path>,
1286    ) -> Result<Arc<dyn DurableStore>, ServerError>;
1287}
1288
1289/// The production factory: the real constructors, recording nothing.
1290pub(super) struct ProductionSubsystems;
1291
1292impl SubsystemFactory for ProductionSubsystems {
1293    fn channel_cluster(
1294        &self,
1295        cluster_config: Option<&ClusterConfig>,
1296    ) -> Result<ChannelCluster, ServerError> {
1297        build_channel_cluster(cluster_config)
1298    }
1299
1300    fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError> {
1301        Ok(Arc::new(ConversationSupervisor::new().map_err(
1302            |error| ServerError::ConfigValidation {
1303                message: format!("failed to start conversation supervisor: {error}"),
1304            },
1305        )?))
1306    }
1307
1308    fn durable_store(
1309        &self,
1310        persistence_path: Option<&Path>,
1311    ) -> Result<Arc<dyn DurableStore>, ServerError> {
1312        build_durable_store(persistence_path)
1313    }
1314}
1315
1316/// Test-only recording implementation of [`SubsystemFactory`] for the §9 D2
1317/// construction gate. Lives here (not in a test module's private scope) so the
1318/// supervisor-level gate test reuses the same instrument.
1319#[cfg(test)]
1320#[allow(clippy::expect_used)]
1321pub(super) mod subsystem_census {
1322    use std::path::{Path, PathBuf};
1323    use std::sync::{Arc, Mutex};
1324
1325    use liminal::conversation::ConversationSupervisor;
1326    use liminal::durability::{DurableStore, open_ephemeral_rooted};
1327
1328    use super::{
1329        ChannelCluster, DEFAULT_SHARD_COUNT, ProductionSubsystems, SchedulerSubsystem,
1330        SubsystemFactory, build_durable_store_with,
1331    };
1332    use crate::ServerError;
1333    use crate::config::types::ClusterConfig;
1334
1335    /// Records each [`SchedulerSubsystem`] AS A SIDE EFFECT of constructing it,
1336    /// then hands back the production-constructed subsystem (with ephemeral
1337    /// stores rooted in an isolated directory, so the fs half of the gate is a
1338    /// real negative assertion). Because [`SubsystemFactory`] is the only route
1339    /// the profile-aware construction path has to these constructors, a
1340    /// recording cannot be omitted without also failing to construct — the
1341    /// record-by-construction guarantee.
1342    pub struct RecordingSubsystems {
1343        census: Mutex<Vec<SchedulerSubsystem>>,
1344        ephemeral_root: PathBuf,
1345    }
1346
1347    impl RecordingSubsystems {
1348        /// A recording factory whose ephemeral stores live under `ephemeral_root`.
1349        pub fn rooted(ephemeral_root: &Path) -> Self {
1350            Self {
1351                census: Mutex::new(Vec::new()),
1352                ephemeral_root: ephemeral_root.to_path_buf(),
1353            }
1354        }
1355
1356        /// The recorded construction census, sorted for order-independent
1357        /// comparison.
1358        pub fn recorded(&self) -> Vec<SchedulerSubsystem> {
1359            let mut recorded = self
1360                .census
1361                .lock()
1362                .expect("subsystem census lock is never poisoned in tests")
1363                .clone();
1364            recorded.sort();
1365            recorded
1366        }
1367
1368        fn record(&self, subsystem: SchedulerSubsystem) {
1369            self.census
1370                .lock()
1371                .expect("subsystem census lock is never poisoned in tests")
1372                .push(subsystem);
1373        }
1374    }
1375
1376    impl SubsystemFactory for RecordingSubsystems {
1377        fn channel_cluster(
1378            &self,
1379            cluster_config: Option<&ClusterConfig>,
1380        ) -> Result<ChannelCluster, ServerError> {
1381            let cluster = ProductionSubsystems.channel_cluster(cluster_config)?;
1382            self.record(SchedulerSubsystem::ChannelSupervisor);
1383            Ok(cluster)
1384        }
1385
1386        fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError> {
1387            let supervisor = ProductionSubsystems.conversation_supervisor()?;
1388            self.record(SchedulerSubsystem::ConversationSupervisor);
1389            Ok(supervisor)
1390        }
1391
1392        fn durable_store(
1393            &self,
1394            persistence_path: Option<&Path>,
1395        ) -> Result<Arc<dyn DurableStore>, ServerError> {
1396            let store = build_durable_store_with(persistence_path, || {
1397                open_ephemeral_rooted(&self.ephemeral_root, DEFAULT_SHARD_COUNT)
1398            })?;
1399            self.record(SchedulerSubsystem::HaematiteStore);
1400            Ok(store)
1401        }
1402    }
1403}
1404
1405/// Full-only constructor guard: rejects a config whose profile is not `Full`.
1406///
1407/// [`LiminalConnectionServices`]' config-based constructors call this at entry so
1408/// the full service stack can never be built for a worker-front-door config —
1409/// profile enforcement holds on every public construction path, not only the
1410/// file-loading pipeline.
1411fn require_full_profile(config: &ServerConfig) -> Result<(), ServerError> {
1412    match config.services.profile()? {
1413        ServiceProfile::Full => Ok(()),
1414        ServiceProfile::WorkerFrontDoor => Err(ServerError::ConfigValidation {
1415            message: format!(
1416                "services.profile: \"{}\" cannot construct the full LiminalConnectionServices; \
1417                 build profile-selected services via build_connection_services",
1418                ServiceProfile::WORKER_FRONT_DOOR
1419            ),
1420        }),
1421    }
1422}
1423
1424/// Builds the connection-services adapter selected by `config`'s service profile.
1425///
1426/// `Full` builds [`LiminalConnectionServices`] (channels, conversations, durable
1427/// store, dedup cache) exactly as today. `WorkerFrontDoor` builds
1428/// [`WorkerFrontDoorServices`], which constructs none of that machinery. This is the
1429/// single profile-dispatch authority: [`super::supervisor::ConnectionSupervisor`]'s
1430/// config constructor and the standalone runtime's worker arm both route through it
1431/// (the runtime's full arm stays on the explicit
1432/// [`LiminalConnectionServices::from_config`] path because it also needs the shared
1433/// channel cluster, which the trait object does not expose — that path is guarded
1434/// full-only at entry).
1435///
1436/// # Errors
1437/// Returns [`ServerError`] when the selected adapter cannot be constructed, the
1438/// configured profile value is not recognised, or the worker-front-door profile is
1439/// combined with full-only config fields.
1440pub fn build_connection_services(
1441    config: &ServerConfig,
1442) -> Result<Arc<dyn ConnectionServices>, ServerError> {
1443    build_connection_services_via(config, &ProductionSubsystems)
1444}
1445
1446/// [`build_connection_services`] with the subsystem factory injected — the §9 D2
1447/// gate seam, both halves at once.
1448///
1449/// Every scheduler-owning subsystem the `Full` branch creates is constructed
1450/// through `subsystems` and nowhere else, so a recording factory observes exactly
1451/// what was built (thread half, record-by-construction); the gate's factory also
1452/// roots its ephemeral stores in an isolated directory, so "the root stays empty
1453/// on the worker branch" is a real negative assertion (fs half, the D3 pattern).
1454/// The `WorkerFrontDoor` branch never touches the factory — a regression that gave
1455/// the front door any subsystem would both record in the census and land a store
1456/// directory in the injected root.
1457///
1458/// The worker branch re-runs the cross-field checks here, not only in file-loading
1459/// validation, so a directly-constructed config cannot smuggle full-only machinery
1460/// past the profile.
1461pub(super) fn build_connection_services_via(
1462    config: &ServerConfig,
1463    subsystems: &dyn SubsystemFactory,
1464) -> Result<Arc<dyn ConnectionServices>, ServerError> {
1465    match config.services.profile()? {
1466        ServiceProfile::Full => {
1467            let store = subsystems.durable_store(config.persistence_path.as_deref())?;
1468            Ok(Arc::new(
1469                LiminalConnectionServices::from_config_with_store_via(config, store, subsystems)?,
1470            ))
1471        }
1472        ServiceProfile::WorkerFrontDoor => {
1473            let errors = crate::config::validation::worker_front_door_field_errors(config);
1474            if !errors.is_empty() {
1475                return Err(ServerError::ConfigValidation {
1476                    message: errors.join("; "),
1477                });
1478            }
1479            Ok(Arc::new(WorkerFrontDoorServices::new()))
1480        }
1481    }
1482}
1483
1484/// Builds the haematite-backed durable store.
1485///
1486/// When `persistence_path` is `Some`, the database lives there and survives
1487/// process restarts: an existing database directory is reopened, a fresh one is
1488/// created. When it is `None` (no durable path configured, or the channel-free
1489/// `empty()` services used by tests), a self-owning ephemeral store is opened
1490/// instead: its temporary directory is created and removed by the store itself
1491/// (D3), so it leaves no residue once the last store handle drops. The two paths
1492/// return distinct concrete stores on purpose — only the ephemeral one carries a
1493/// directory guard; the persistent path is untouched.
1494fn build_durable_store(
1495    persistence_path: Option<&Path>,
1496) -> Result<Arc<dyn DurableStore>, ServerError> {
1497    build_durable_store_with(persistence_path, || open_ephemeral(DEFAULT_SHARD_COUNT))
1498}
1499
1500/// [`build_durable_store`] with the ephemeral factory injected.
1501///
1502/// The split exists for the D3 construction gates: the SAME branch logic runs
1503/// in production and tests, and only the factory closure differs — tests root
1504/// the ephemeral store in an isolated directory (via liminal's test-gated
1505/// rooted factory) so "no ephemeral directory was created" is a real assertion
1506/// rather than a scan of the shared system temp dir.
1507fn build_durable_store_with(
1508    persistence_path: Option<&Path>,
1509    make_ephemeral: impl FnOnce() -> Result<EphemeralHaematiteStore, DurabilityError>,
1510) -> Result<Arc<dyn DurableStore>, ServerError> {
1511    let Some(path) = persistence_path else {
1512        let store = make_ephemeral().map_err(|error| ServerError::ConfigValidation {
1513            message: format!("failed to open ephemeral durable store: {error}"),
1514        })?;
1515        return Ok(Arc::new(store));
1516    };
1517    let data_dir = path.join("durability");
1518    let database = open_or_create_database(&data_dir)?;
1519    let event_store = EventStore::new(database);
1520    Ok(Arc::new(HaematiteStore::new(Arc::new(event_store))))
1521}
1522
1523/// Opens an existing haematite database at `data_dir`, or creates one.
1524fn open_or_create_database(data_dir: &Path) -> Result<Database, ServerError> {
1525    let config_file = data_dir.join("config.json");
1526    let result = if config_file.exists() {
1527        Database::open(data_dir)
1528    } else {
1529        Database::create(DatabaseConfig {
1530            data_dir: data_dir.to_path_buf(),
1531            shard_count: DEFAULT_SHARD_COUNT,
1532            distributed: None,
1533            executor_threads: None,
1534            // haematite 0.8.3 requires this field and refuses `None` at
1535            // validation; `Unlimited` is the crate's explicit spelling of the
1536            // pre-budget (0.8.1) behaviour — the behaviour-preserving choice
1537            // for a dependency hop. A real byte ceiling is a deployment
1538            // decision for its own measured lane. ⚠ The `Database::open` arm
1539            // above is unaffected on CREATE but a store whose `config.json`
1540            // predates this field refuses to open (`MissingNodeCacheBudget`)
1541            // until an operator writes a budget into that file — loud by
1542            // haematite's design, named in this hop's lane report.
1543            node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
1544        })
1545    };
1546    result.map_err(|error| ServerError::ConfigValidation {
1547        message: format!(
1548            "failed to open durable store at {}: {error}",
1549            data_dir.display()
1550        ),
1551    })
1552}
1553
1554impl ConnectionServices for LiminalConnectionServices {
1555    fn participant_service(&self) -> Option<InstalledParticipantService> {
1556        self.participant_service.clone()
1557    }
1558
1559    /// The roster's admission decision, exposed across the trait boundary as the
1560    /// permission alone.
1561    ///
1562    /// Two methods share the name `admit_channel` on this type: this trait
1563    /// method, and the private inherent funnel it delegates to. They are the
1564    /// same decision at two different boundaries — the funnel returns the ENTRY
1565    /// the decision was made on, because its in-crate callers go on to publish
1566    /// or subscribe through it, and this one returns `()`, because a caller
1567    /// outside the crate is asking whether it MAY, not for the thing itself.
1568    /// Handing the entry out here would make [`ConfiguredChannel`] public
1569    /// surface and hand a frame-level caller a channel handle it has no business
1570    /// holding. Rust resolves an inherent method ahead of a trait method of the
1571    /// same name, so the funnel's existing callers — and the `Self::` call below
1572    /// — reach the funnel, not this method; the trait form is reached only
1573    /// through a `dyn ConnectionServices`, which is exactly the caller it exists
1574    /// for. `an_admitted_operation_the_service_then_refuses_stays_undifferentiated`
1575    /// is the instrument that would catch this delegation turning into a
1576    /// recursion.
1577    ///
1578    /// `operation` is unused in v1 and the parameter is still right: absence
1579    /// refuses both operations, and quiesce refuses new publishes and new
1580    /// subscribes alike, so the two answers are equal today and not equal by
1581    /// definition. A signature that could not see what it was deciding about
1582    /// would have to break the day they part.
1583    fn admit_channel(
1584        &self,
1585        _operation: ChannelOperation,
1586        channel: &str,
1587    ) -> Result<(), ChannelAccessError> {
1588        Self::admit_channel(self, channel)?;
1589        Ok(())
1590    }
1591
1592    fn publish(
1593        &self,
1594        channel: &str,
1595        envelope: &MessageEnvelope,
1596        idempotency_key: Option<&str>,
1597    ) -> Result<PublishOutcome, ServerError> {
1598        // The roster read holds its guard only long enough to clone the entry's
1599        // `Arc` out; everything below — the dedup bridge and the publish into the
1600        // channel actor — runs with no roster lock held.
1601        //
1602        // This inner admission STAYS even once the connection process consults
1603        // the roster ahead of delegating: this method is public and callable
1604        // without going through a frame at all, and a guard that only exists in
1605        // the caller is not a guard.
1606        let configured = self
1607            .admit_channel(channel)
1608            .map_err(|error| access_to_server_error(&error))?;
1609
1610        // Dedup-on-delivery: a publish carrying an idempotency key is delivered to
1611        // subscribers AT MOST ONCE across re-publishes of the same key. Only a
1612        // fresh `Claimed` decision proceeds to fan-out; a `Completed`/`InFlight`
1613        // decision is a duplicate and is suppressed (no second delivery), which is
1614        // the at-most-once guarantee the aion outbox relies on.
1615        if let Some(key) = idempotency_key {
1616            if !self.claim_delivery(key)? {
1617                // A dedup-suppressed re-publish is still an accepted publish (it is
1618                // assigned a message id), but it reaches no subscriber, so it counts
1619                // toward publishes and not deliveries.
1620                crate::metrics::publish_accepted();
1621                return Ok(PublishOutcome {
1622                    message_id: self.next_message_id.fetch_add(1, Ordering::Relaxed),
1623                    delivered: false,
1624                });
1625            }
1626        }
1627
1628        let delivery = configured.handle.publish_with_delivery(
1629            &envelope.payload,
1630            liminal::envelope::PublisherId::default(),
1631            None,
1632        );
1633        let delivery = match delivery {
1634            Ok(delivery) => delivery,
1635            Err(error) => {
1636                // The claim above appended an `InFlight` entry but the delivery
1637                // failed before `complete_receipt` could run. Release the claim so
1638                // the key is re-claimable; otherwise every re-publish would see
1639                // `InFlight` and be suppressed forever. Best-effort: surface the
1640                // ORIGINAL publish error regardless, but never swallow a release
1641                // failure silently (it leaves the leak intact).
1642                if let Some(key) = idempotency_key {
1643                    self.release_claim(key);
1644                }
1645                return Err(ServerError::ListenerAccept {
1646                    message: format!("liminal publish failed for channel '{channel}': {error}"),
1647                });
1648            }
1649        };
1650
1651        // Record the dedup completion AFTER a successful claimed delivery so the
1652        // claim is not left dangling `InFlight` (which would wrongly defer every
1653        // future duplicate). The receipt body is empty: the dedup contract here
1654        // only needs presence, not a stored result.
1655        if let Some(key) = idempotency_key {
1656            block_on(
1657                self.dedup
1658                    .complete_receipt(key, ProcessingReceipt::new(Vec::new())),
1659            )
1660            .map_err(|error| ServerError::ListenerAccept {
1661                message: format!("dedup receipt bridge failed for key '{key}': {error}"),
1662            })?
1663            .map_err(|error| ServerError::ListenerAccept {
1664                message: format!("dedup receipt write failed for key '{key}': {error}"),
1665            })?;
1666        }
1667
1668        // Record the accepted publish and its genuine subscriber deliveries. The
1669        // delivered count (0 for an empty channel) is the same signal the delivery
1670        // ack is derived from.
1671        crate::metrics::publish_accepted();
1672        let delivered_count = u64::try_from(delivery.delivered_count()).unwrap_or(u64::MAX);
1673        crate::metrics::deliveries_recorded(delivered_count);
1674
1675        Ok(PublishOutcome {
1676            message_id: self.next_message_id.fetch_add(1, Ordering::Relaxed),
1677            delivered: delivery.is_delivered(),
1678        })
1679    }
1680
1681    fn subscribe(
1682        &self,
1683        channel: &str,
1684        accepted_schemas: &[ProtocolSchemaId],
1685        install: Option<liminal::channel::InboxInstall>,
1686    ) -> Result<ConnectionSubscription, ServerError> {
1687        // As in `publish`: the admission funnel clones the entry's `Arc` out
1688        // under the read guard and the guard is released before schema
1689        // negotiation or the actor round-trip below. That release is what makes
1690        // this the linearisation point — a quiesce landing after it does not
1691        // stop the subscription this call is already building.
1692        let configured = self
1693            .admit_channel(channel)
1694            .map_err(|error| access_to_server_error(&error))?;
1695        let selected_schema = if accepted_schemas.is_empty() {
1696            configured.protocol_schema
1697        } else {
1698            liminal::protocol::negotiate_schema(configured.protocol_schema, accepted_schemas)
1699                .map_err(|error| server_error_from_protocol(&error))?
1700        };
1701        // `subscribe_with_install` installs the §5 budget/fairness cap and the R3
1702        // wake notifier on the inbox at construction — strictly before the
1703        // registration is published to the channel actor — so there is no window
1704        // in which a publish can land uncharged or without a wake.
1705        let subscription = install
1706            .map_or_else(
1707                || configured.handle.subscribe(),
1708                |install| configured.handle.subscribe_with_install(install),
1709            )
1710            .map_err(|error| ServerError::ListenerAccept {
1711                message: format!("liminal subscribe failed for channel '{channel}': {error}"),
1712            })?;
1713        let id = self.next_subscription_id.fetch_add(1, Ordering::Relaxed);
1714        let mut connection_subscription = ConnectionSubscription::new(
1715            id,
1716            selected_schema,
1717            Box::new(LiminalSubscriptionResource { subscription }),
1718        );
1719        connection_subscription.set_channel(channel.to_owned());
1720        Ok(connection_subscription)
1721    }
1722
1723    fn unsubscribe(&self, subscription: ConnectionSubscription) -> Result<(), ServerError> {
1724        subscription.unsubscribe()
1725    }
1726
1727    fn open_conversation(
1728        &self,
1729        conversation_id: u64,
1730        subject: &str,
1731    ) -> Result<ConnectionConversation, ServerError> {
1732        // Spawn a REAL participant process (a beamr `NativeHandler` running the
1733        // resolved responder behaviour) on the conversation supervisor's
1734        // scheduler, and a supervised conversation actor linked to it. The actor
1735        // FORWARDS each conversation message to the participant, which genuinely
1736        // processes it and delivers a reply back. The actor traps the
1737        // participant's EXIT (a beamr process link), so killing it fires a
1738        // structural, microsecond-scale crash signal.
1739        //
1740        // The responder is chosen by `subject`: a custom responder registered via
1741        // `register_responder` for this subject, or the built-in `EchoBehaviour`
1742        // when none is registered. Either way it runs as the SAME supervised,
1743        // linked participant process — the seam changes WHO responds, not HOW the
1744        // participant is spawned or supervised.
1745        let behaviour = self.responder_for(subject)?;
1746        let (actor, participant) = self
1747            .conversation_supervisor
1748            .spawn_with_participant(behaviour, None, ChannelMode::Ephemeral, CrashPolicy::Fail)
1749            .map_err(|error| ServerError::ListenerAccept {
1750                message: format!(
1751                    "failed to spawn supervised conversation {conversation_id} ('{subject}'): {error}"
1752                ),
1753            })?;
1754
1755        // Drive boot to completion so the beamr link to the participant exists
1756        // before any message is forwarded (link-before-forward), mirroring the
1757        // ROUTING-004 dispatch pattern.
1758        actor.pid().map_err(|error| ServerError::ListenerAccept {
1759            message: format!(
1760                "failed to boot supervised conversation {conversation_id} ('{subject}'): {error}"
1761            ),
1762        })?;
1763
1764        // Register the structural EXIT notifier BEFORE returning, so a crash that
1765        // fires the instant a message reaches the participant is never missed.
1766        // The notifier is woken by the actor's trapped-EXIT handler (event
1767        // driven), and a crash that already landed is replayed immediately.
1768        let (exit_tx, exit_rx) = mpsc::sync_channel::<Instant>(1);
1769        actor
1770            .notify_on_participant_exit(participant, exit_tx)
1771            .map_err(|error| ServerError::ListenerAccept {
1772                message: format!(
1773                    "failed to arm crash detection for conversation {conversation_id}: {error}"
1774                ),
1775            })?;
1776
1777        Ok(ConnectionConversation::new(Box::new(
1778            LiminalConversationResource::new(actor, participant, exit_rx),
1779        )))
1780    }
1781
1782    fn conversation_message(
1783        &self,
1784        conversation: &ConnectionConversation,
1785        envelope: &MessageEnvelope,
1786    ) -> Result<(), ServerError> {
1787        conversation.message(envelope)
1788    }
1789
1790    fn close_conversation(&self, conversation: ConnectionConversation) -> Result<(), ServerError> {
1791        conversation.close()
1792    }
1793
1794    fn flush_durable_state(&self) -> Result<(), ServerError> {
1795        // Clone the roster out under the read guard and drop the guard before
1796        // flushing: a shutdown flush is a durable write per entry, and it must
1797        // never run with the roster lock held.
1798        let entries: Vec<(String, Arc<ConfiguredChannel>)> = {
1799            let channels = self.read_channels()?;
1800            channels
1801                .iter()
1802                .map(|(channel_name, configured)| (channel_name.clone(), Arc::clone(configured)))
1803                .collect()
1804        };
1805        for (channel_name, configured) in entries {
1806            if configured.handle.config().mode == ChannelMode::Durable {
1807                configured
1808                    .handle
1809                    .flush()
1810                    .map_err(|error| ServerError::ShutdownFlush {
1811                        message: format!(
1812                            "failed to flush durable channel '{channel_name}': {error}"
1813                        ),
1814                    })?;
1815            }
1816        }
1817        Ok(())
1818    }
1819}
1820
1821/// One roster entry: a channel's library handle, the protocol schema id
1822/// advertised for it, where it came from, and its admission state.
1823#[derive(Debug)]
1824pub(super) struct ConfiguredChannel {
1825    handle: ChannelHandle,
1826    protocol_schema: ProtocolSchemaId,
1827    /// Boot-configured or runtime-registered. Written once at construction and
1828    /// NEVER flipped: it is the only field that predicts what a restart does to
1829    /// this entry, and it defines the population the registration cap counts.
1830    origin: ChannelOrigin,
1831    /// [`STATE_ACTIVE`] or [`STATE_QUIESCED`]. Written at most once, by one
1832    /// `compare_exchange`, so the transition is one-way and exactly one caller
1833    /// can perform it.
1834    state: AtomicU8,
1835    /// The quiesce cause, set STRICTLY BEFORE `state` flips so any reader that
1836    /// observes [`STATE_QUIESCED`] can read it. Written once (the `OnceLock`
1837    /// enforces that structurally), which is why a re-quiesce under a different
1838    /// reason must refuse rather than silently keep or replace one of them.
1839    quiesce_reason: OnceLock<String>,
1840}
1841
1842impl ConfiguredChannel {
1843    /// The entry's admission state as a value.
1844    ///
1845    /// One `Acquire` load and, when quiesced, one read of the already-written
1846    /// reason. Touches no actor: the whole point of recording state on the entry
1847    /// is that observing it cannot spawn the thing being observed.
1848    fn state(&self) -> ChannelState {
1849        if self.state.load(Ordering::Acquire) == STATE_QUIESCED {
1850            ChannelState::Quiesced {
1851                reason: self.recorded_quiesce_reason(),
1852            }
1853        } else {
1854            ChannelState::Active
1855        }
1856    }
1857
1858    /// The recorded quiesce reason, for a caller that has already observed
1859    /// [`STATE_QUIESCED`] with an `Acquire` load.
1860    fn recorded_quiesce_reason(&self) -> String {
1861        self.quiesce_reason
1862            .get()
1863            .map_or_else(|| UNRECORDED_QUIESCE_REASON.to_owned(), Clone::clone)
1864    }
1865
1866    /// Moves this entry from active to quiesced, recording `reason` first.
1867    ///
1868    /// The reason is written to the `OnceLock` BEFORE the `Release`
1869    /// `compare_exchange` that flips the state, so no reader can observe
1870    /// `QUIESCED` without being able to read why. Re-quiescing under the
1871    /// IDENTICAL reason is `Ok(())` — the caller's intent already holds — and a
1872    /// DIFFERENT reason refuses, because the recorded one cannot be replaced
1873    /// without losing a cause and cannot be kept without lying to the caller.
1874    fn quiesce(&self, name: &str, reason: &str) -> Result<(), ChannelRegistryError> {
1875        // Whoever wins the `OnceLock` writes the cause on record; every other
1876        // caller — a re-quiesce or a concurrent racer — is judged against THAT
1877        // reason, never against whether it happened to perform the flip itself.
1878        // Reporting success for having won the CAS would tell a racer whose
1879        // reason lost that its reason took effect.
1880        let recorded = match self.quiesce_reason.set(reason.to_owned()) {
1881            Ok(()) => reason.to_owned(),
1882            Err(_rejected) => self.recorded_quiesce_reason(),
1883        };
1884        // The reason is now readable, so the flip may become visible. The CAS
1885        // result is deliberately unused: the state is `QUIESCED` afterwards
1886        // whether this call or a racer performed the transition, and the answer
1887        // to the caller is governed by the recorded reason above.
1888        let _flipped_here = self.state.compare_exchange(
1889            STATE_ACTIVE,
1890            STATE_QUIESCED,
1891            Ordering::Release,
1892            Ordering::Acquire,
1893        );
1894        if recorded == reason {
1895            return Ok(());
1896        }
1897        Err(ChannelRegistryError::AlreadyQuiesced {
1898            name: name.to_owned(),
1899            reason: recorded,
1900        })
1901    }
1902}
1903
1904#[derive(Debug)]
1905struct LiminalSubscriptionResource {
1906    subscription: liminal::channel::SubscriptionHandle,
1907}
1908
1909impl SubscriptionResource for LiminalSubscriptionResource {
1910    fn unsubscribe(self: Box<Self>) -> Result<(), ServerError> {
1911        drop(self.subscription);
1912        Ok(())
1913    }
1914
1915    fn is_overflowed(&self) -> bool {
1916        self.subscription.is_overflowed()
1917    }
1918
1919    fn has_pending(&self) -> bool {
1920        self.subscription.has_pending()
1921    }
1922
1923    fn try_next(&mut self) -> Option<liminal::envelope::Envelope> {
1924        match self.subscription.try_next() {
1925            Ok(envelope) => envelope,
1926            Err(error) => {
1927                // A poisoned inbox lock is PERMANENT, not transient: once poisoned it
1928                // stays poisoned, so every future `try_next` also returns `Err` and this
1929                // subscription goes silent for the rest of its life — no further
1930                // deliveries, not "held for the next slice". Poisoning requires a panic
1931                // while the lock is held, which the workspace lints forbid
1932                // (no unwrap/expect/panic), so this is an accepted low-probability
1933                // failure rather than a recoverable one. We keep the connection alive (a
1934                // single permanently-silent subscription is less harmful than tearing
1935                // down every other subscription and stream the connection multiplexes)
1936                // but log loudly so the silence is diagnosable. The log cannot storm:
1937                // it can only fire after the one panic that poisoned the lock.
1938                tracing::error!(
1939                    %error,
1940                    "subscription inbox lock is poisoned; this subscription is now \
1941                     permanently silent and will deliver no further messages"
1942                );
1943                None
1944            }
1945        }
1946    }
1947}
1948
1949/// Renders an admission refusal as the service error the trait's callers expect.
1950///
1951/// The two refusals that already existed keep their EXACT bytes: an absent
1952/// channel is still `channel '<name>' is not configured` and a poisoned roster
1953/// is still `channel roster lock poisoned`, so nothing that reads these messages
1954/// today sees a change. Discrimination lives in the reason code
1955/// ([`ChannelAccessError::reason_code`]), which the connection process carries to
1956/// the wire — this rendering is the degraded path for a caller that reaches the
1957/// service directly and has no code to carry.
1958///
1959/// `Quiesced` has no predecessor string to preserve; it carries the refusal's own
1960/// message, reason included.
1961///
1962/// The connection process renders its admission refusals through this SAME
1963/// function rather than through [`ChannelAccessError`]'s own `Display`. Two
1964/// consequences, both wanted. The bytes on the wire for an absent channel stay
1965/// exactly what they have always been, which is the whole of the semver promise
1966/// this lane makes. And an operation refused AT admission and one refused after
1967/// admission by the service produce the identical message, differing only in the
1968/// reason code — so the code really is the only discriminator, instead of the
1969/// message quietly becoming a second one.
1970pub(super) fn access_to_server_error(error: &ChannelAccessError) -> ServerError {
1971    let message = match error {
1972        ChannelAccessError::NotRegistered { name } => {
1973            format!("channel '{name}' is not configured")
1974        }
1975        ChannelAccessError::Quiesced { .. } => error.to_string(),
1976        ChannelAccessError::RosterUnavailable { message } => message.clone(),
1977    };
1978    ServerError::ListenerAccept { message }
1979}
1980
1981pub(super) fn server_error_from_protocol(error: &ProtocolError) -> ServerError {
1982    ServerError::ListenerAccept {
1983        message: format!("protocol operation failed: {error}"),
1984    }
1985}
1986
1987/// The three pinned registration tests. A CHILD module of `services` because
1988/// test 2 must hold the exact entry the admission funnel handed out.
1989#[cfg(test)]
1990#[path = "services_registry_tests.rs"]
1991mod services_registry_tests;
1992
1993#[cfg(test)]
1994#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
1995mod durable_store_tests {
1996    use liminal::durability::open_ephemeral_rooted;
1997
1998    use super::subsystem_census::RecordingSubsystems;
1999    use super::{
2000        ConnectionServices, DEFAULT_SHARD_COUNT, LiminalConnectionServices, SchedulerSubsystem,
2001        build_connection_services, build_connection_services_via, build_durable_store_with,
2002    };
2003    use crate::ServerError;
2004    use crate::config::types::{LimitsConfig, ServerConfig, ServicesConfig};
2005
2006    /// Counts directory entries under `root`, for the empty/one-dir assertions
2007    /// on an injected ephemeral root.
2008    fn entry_count(root: &std::path::Path) -> usize {
2009        std::fs::read_dir(root)
2010            .expect("ephemeral root is readable")
2011            .count()
2012    }
2013
2014    /// A minimal channel-free config with the given service `profile`. No channels,
2015    /// routing, persistence, or cluster — the shape both profiles accept (the full
2016    /// profile simply builds an empty channel set; the worker-front-door profile
2017    /// requires exactly this shape).
2018    fn config_with_profile(profile: &str) -> ServerConfig {
2019        ServerConfig {
2020            listen_address: "127.0.0.1:0".parse().expect("valid socket addr"),
2021            health_listen_address: "127.0.0.1:1".parse().expect("valid socket addr"),
2022            drain_timeout_ms: 30_000,
2023            channels: Vec::new(),
2024            routing_rules: Vec::new(),
2025            persistence_path: None,
2026            cluster: None,
2027            auth: None,
2028            services: ServicesConfig {
2029                profile: profile.to_owned(),
2030            },
2031            limits: LimitsConfig::default(),
2032            participant: None,
2033            websocket: None,
2034        }
2035    }
2036
2037    /// §9 D2 front-door construction gate (fs half): building the worker-front-door
2038    /// services creates NO haematite store and NO temp dir, while the full profile
2039    /// over an equally-rooted factory DOES create exactly one store directory.
2040    ///
2041    /// The injected root is the only place an ephemeral store directory can appear
2042    /// (the recording factory roots its stores there), so "the root stays empty on
2043    /// the front-door branch" is a real negative assertion: a regression that gave
2044    /// the front door a store would land its directory here and fail this test. The
2045    /// full-profile arm is the positive control proving the seam genuinely
2046    /// constructs a store when the profile asks for one.
2047    #[test]
2048    fn worker_front_door_builds_no_store_and_no_temp_dir() {
2049        let front_door_root = tempfile::tempdir().expect("test can create an ephemeral root");
2050        let full_root = tempfile::tempdir().expect("test can create an ephemeral root");
2051
2052        let front_door_subsystems = RecordingSubsystems::rooted(front_door_root.path());
2053        let front_door: std::sync::Arc<dyn ConnectionServices> = build_connection_services_via(
2054            &config_with_profile("worker-front-door"),
2055            &front_door_subsystems,
2056        )
2057        .expect("worker-front-door services build");
2058        assert!(
2059            !front_door.supports_channel_operations(),
2060            "the worker front door serves no channel operations"
2061        );
2062        assert_eq!(
2063            entry_count(front_door_root.path()),
2064            0,
2065            "the worker front door creates no ephemeral store directory (no haematite, no temp dir)"
2066        );
2067
2068        let full_subsystems = RecordingSubsystems::rooted(full_root.path());
2069        let full = build_connection_services_via(&config_with_profile("full"), &full_subsystems)
2070            .expect("full services build");
2071        assert!(
2072            full.supports_channel_operations(),
2073            "full mode serves channel operations"
2074        );
2075        assert_eq!(
2076            entry_count(full_root.path()),
2077            1,
2078            "full mode with no persistence path builds exactly one ephemeral store directory"
2079        );
2080
2081        drop(front_door);
2082        drop(full);
2083    }
2084
2085    /// §9 D2 front-door construction gate (thread half — record-by-construction
2086    /// census): the worker profile constructs NO channel-supervisor,
2087    /// conversation-supervisor, or haematite scheduler, while the SAME instrument
2088    /// over the full profile records all three — the positive control proving the
2089    /// census detects the extra schedulers, so an empty census on the worker branch
2090    /// is a real observation, not a decoration.
2091    ///
2092    /// The instrument's boundary: recording happens INSIDE the [`SubsystemFactory`]
2093    /// methods that are the profile-aware path's only route to these constructors,
2094    /// so a recording cannot be silently omitted — a future subsystem added to this
2095    /// path either goes through the factory (and is recorded by construction) or
2096    /// bypasses it, which is a code-review-visible structural violation of the
2097    /// factory seam, not a silently-missing side call. The connection supervisor's
2098    /// own scheduler is the shared baseline of both profiles and is asserted at the
2099    /// supervisor level (`supervisor::tests`); an OS-level thread census upgrades
2100    /// this when the beamr composition lane's scheduler-inventory API (currently on
2101    /// their branch, not yet consumable from liminal) lands.
2102    #[test]
2103    fn worker_profile_census_is_empty_and_full_profile_records_all_schedulers() {
2104        let worker_root = tempfile::tempdir().expect("test can create an ephemeral root");
2105        let full_root = tempfile::tempdir().expect("test can create an ephemeral root");
2106
2107        let worker_subsystems = RecordingSubsystems::rooted(worker_root.path());
2108        let front_door = build_connection_services_via(
2109            &config_with_profile("worker-front-door"),
2110            &worker_subsystems,
2111        )
2112        .expect("worker-front-door services build");
2113        assert_eq!(
2114            worker_subsystems.recorded(),
2115            Vec::<SchedulerSubsystem>::new(),
2116            "the worker front door constructs no scheduler-owning subsystem"
2117        );
2118
2119        let full_subsystems = RecordingSubsystems::rooted(full_root.path());
2120        let full = build_connection_services_via(&config_with_profile("full"), &full_subsystems)
2121            .expect("full services build");
2122        assert_eq!(
2123            full_subsystems.recorded(),
2124            vec![
2125                SchedulerSubsystem::ChannelSupervisor,
2126                SchedulerSubsystem::ConversationSupervisor,
2127                SchedulerSubsystem::HaematiteStore,
2128            ],
2129            "the full profile constructs every scheduler-owning subsystem, once each — \
2130             the positive control proving the census instrument detects them"
2131        );
2132
2133        drop(front_door);
2134        drop(full);
2135    }
2136
2137    /// MAJOR-1 regression: the full-only constructors reject a worker-front-door
2138    /// config with a typed `ConfigValidation` error AT ENTRY — no full service can
2139    /// be created through any public config-based constructor under that profile.
2140    #[test]
2141    fn full_only_constructors_reject_worker_profile() {
2142        let config = config_with_profile("worker-front-door");
2143
2144        let from_config = LiminalConnectionServices::from_config(&config);
2145        assert!(
2146            matches!(from_config, Err(ServerError::ConfigValidation { .. })),
2147            "from_config must reject a worker-front-door profile with ConfigValidation, got {from_config:?}"
2148        );
2149
2150        let root = tempfile::tempdir().expect("test can create an ephemeral root");
2151        let store = open_ephemeral_rooted(root.path(), DEFAULT_SHARD_COUNT)
2152            .expect("test store for the rejection check builds");
2153        let from_config_with_store =
2154            LiminalConnectionServices::from_config_with_store(&config, std::sync::Arc::new(store));
2155        assert!(
2156            matches!(
2157                from_config_with_store,
2158                Err(ServerError::ConfigValidation { .. })
2159            ),
2160            "from_config_with_store must reject a worker-front-door profile with ConfigValidation"
2161        );
2162    }
2163
2164    /// MAJOR-1 regression: the profile-aware factory itself re-runs the
2165    /// worker-front-door cross-field checks, so a directly-constructed config (one
2166    /// that never passed file-loading validation) combining the worker profile with
2167    /// full-only machinery is refused with the same typed `ConfigValidation` errors.
2168    #[test]
2169    fn build_connection_services_rejects_worker_profile_with_full_only_fields() {
2170        let mut config = config_with_profile("worker-front-door");
2171        config.channels = vec![crate::config::types::ChannelDef {
2172            name: "orders".to_owned(),
2173            schema_ref: None,
2174            durable: false,
2175            loaded_schema: None,
2176        }];
2177        config.persistence_path = Some(std::path::PathBuf::from("/tmp"));
2178
2179        let result = build_connection_services(&config);
2180        let Err(ServerError::ConfigValidation { message }) = result else {
2181            panic!("expected ConfigValidation for worker profile with full-only fields");
2182        };
2183        assert!(message.contains("builds no channels"), "got: {message}");
2184        assert!(
2185            message.contains("builds no durable store"),
2186            "got: {message}"
2187        );
2188    }
2189
2190    /// §9 D3 construction gate (persistent half): requesting a *persistent* store
2191    /// creates its database under the configured path and NO ephemeral directory.
2192    ///
2193    /// Exercises `build_durable_store_with` — the same branch logic production
2194    /// runs — with only the ephemeral factory swapped to root in an isolated
2195    /// directory. That root is where any ephemeral directory would have to
2196    /// appear, so "the root stays empty" is a real negative assertion — a
2197    /// regression that constructs an ephemeral store on the persistent branch
2198    /// lands its directory here and fails this test.
2199    #[test]
2200    fn persistent_store_uses_configured_path_and_creates_no_temp_dir() {
2201        let home = tempfile::tempdir().expect("test can create a temp dir");
2202        let ephemeral_root = tempfile::tempdir().expect("test can create an ephemeral root");
2203
2204        let store = build_durable_store_with(Some(home.path()), || {
2205            open_ephemeral_rooted(ephemeral_root.path(), DEFAULT_SHARD_COUNT)
2206        })
2207        .expect("persistent store builds");
2208
2209        assert!(
2210            home.path().join("durability").join("config.json").exists(),
2211            "the persistent database is created under the configured path"
2212        );
2213        assert_eq!(
2214            entry_count(ephemeral_root.path()),
2215            0,
2216            "the persistent branch creates no ephemeral guard directory"
2217        );
2218
2219        drop(store);
2220    }
2221
2222    /// Pins the wiring seam: the ephemeral (`None`) branch of the shared build
2223    /// logic goes through the guarded constructor — exactly one directory
2224    /// appears under the injected root while the store lives, and zero residue
2225    /// remains after the last handle drops.
2226    #[test]
2227    fn ephemeral_store_directory_is_owned_through_the_build_seam() {
2228        let ephemeral_root = tempfile::tempdir().expect("test can create an ephemeral root");
2229
2230        let store = build_durable_store_with(None, || {
2231            open_ephemeral_rooted(ephemeral_root.path(), DEFAULT_SHARD_COUNT)
2232        })
2233        .expect("ephemeral store builds");
2234
2235        assert_eq!(
2236            entry_count(ephemeral_root.path()),
2237            1,
2238            "the ephemeral branch creates exactly one guard directory"
2239        );
2240
2241        drop(store);
2242
2243        assert_eq!(
2244            entry_count(ephemeral_root.path()),
2245            0,
2246            "dropping the last store handle removes the guard directory — zero residue"
2247        );
2248    }
2249}