Skip to main content

liminal_server/server/connection/
services.rs

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