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