Skip to main content

liminal_server/server/connection/
services.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
4use std::sync::{Arc, Mutex, OnceLock, RwLock};
5use std::time::Instant;
6
7use haematite::{CommitPolicy, Database, DatabaseConfig, EventStore, Retention};
8use liminal::channel::{
9    ChannelConfig, ChannelDelivery, ChannelHandle, ChannelMode, ChannelSupervisor, Schema,
10};
11use liminal::conversation::{ConversationSupervisor, ParticipantBehaviour};
12use liminal::durability::bridge::block_on;
13#[cfg(any(test, feature = "test-support"))]
14use liminal::durability::open_ephemeral;
15use liminal::durability::{
16    CountingStore, DedupCache, DedupDecision, DurableStore, EphemeralRoot, EphemeralSweepCounts,
17    HaematiteStore, ProcessingReceipt, StoreCountsHandle,
18};
19use liminal::protocol::{MessageEnvelope, ProtocolError, SchemaId as ProtocolSchemaId};
20use std::num::NonZeroU64;
21
22use super::channel_registry::{
23    ChannelAccessError, ChannelBuildError, ChannelConfigField, ChannelDescriptor, ChannelOrigin,
24    ChannelRegistration, ChannelRegistryError, ChannelState, ChannelStatus, MAX_CHANNELS_KEY,
25    Registered, STATE_ACTIVE, STATE_QUIESCED, UNRECORDED_QUIESCE_REASON,
26};
27use super::conversation::ConnectionConversation;
28use super::services_cluster::build_channel_cluster;
29use super::services_schema::{ChannelSchema, resolve_channel_schema, resolve_schema_bytes};
30use super::worker_front_door::WorkerFrontDoorServices;
31use crate::ServerError;
32use crate::config::types::{ClusterConfig, ParticipantConfig, ServerConfig, ServiceProfile};
33use crate::config::validation::required_persistence_path;
34use crate::health::reissue::OperatorCredentialReissuer;
35use crate::health::unloadable::UnloadableConversationRecord;
36use crate::server::participant::{
37    InstalledParticipantService, ParticipantWarm, ProductionParticipantHandler,
38};
39use crate::server::self_streams::events::PublishOrigin;
40use crate::server::self_streams::sink::OriginPublication;
41use crate::server::signals::ServerSignals;
42
43pub use super::services_cluster::ChannelCluster;
44
45#[path = "services_lifecycle.rs"]
46mod lifecycle;
47#[path = "services_conversation.rs"]
48mod services_conversation;
49
50/// Registry of custom conversation responders, keyed by conversation subject.
51///
52/// A registered [`ParticipantBehaviour`] becomes the participant for any
53/// conversation opened on its subject; an unregistered subject has no local server.
54type ResponderRegistry = HashMap<String, Arc<dyn ParticipantBehaviour>>;
55
56/// Marker for resources retained by a connection process until unsubscribe.
57pub trait SubscriptionResource: std::fmt::Debug + Send {
58    /// Releases the library subscription resource.
59    ///
60    /// # Errors
61    /// Returns [`ServerError`] when the liminal library reports an unsubscribe failure.
62    fn unsubscribe(self: Box<Self>) -> Result<(), ServerError>;
63
64    /// Attempts to pull the next delivered envelope from the wrapped library
65    /// subscription without blocking.
66    ///
67    /// Returns `None` when the subscriber inbox is empty (or momentarily
68    /// unavailable): the connection process is the delivery pump, so a transient
69    /// empty read is simply "nothing to deliver this slice", never an error.
70    fn try_next(&mut self) -> Option<liminal::envelope::Envelope>;
71
72    /// Non-consuming availability query for the post-arm race barrier.
73    fn has_pending(&self) -> bool;
74
75    /// Whether an overflow has marked this subscription for shedding (§5). The
76    /// delivery pump sheds an overflowed subscription with a typed error frame.
77    /// Defaulted to `false`: a resource with no bounded inbox never overflows.
78    fn is_overflowed(&self) -> bool {
79        false
80    }
81
82    /// The library subscriber's pid, the id a recording publish names it by.
83    /// Defaulted to `None`: a resource with no library subscriber is never a
84    /// recipient.
85    fn subscriber_pid(&self) -> Option<u64> {
86        None
87    }
88
89    /// Reads a delivery and its durable position without acknowledging it.
90    ///
91    /// # Errors
92    /// The underlying subscription cannot be read.
93    fn try_next_positioned(
94        &mut self,
95    ) -> Result<Option<(liminal::envelope::Envelope, Option<u64>)>, ServerError> {
96        Ok(self.try_next().map(|envelope| (envelope, None)))
97    }
98
99    /// Checkpoints a delivery after its consumer settles it.
100    ///
101    /// # Errors
102    /// The resource has no durable acknowledgement path, or checkpointing failed.
103    fn acknowledge_position(&self, position: u64) -> Result<(), ServerError> {
104        Err(ServerError::ListenerAccept {
105            message: format!("subscription cannot acknowledge durable position {position}"),
106        })
107    }
108
109    /// Reads the subscription-owned durable checkpoint.
110    ///
111    /// # Errors
112    /// The checkpoint cannot be read.
113    fn consumer_position(&self) -> Result<Option<u64>, ServerError> {
114        Ok(None)
115    }
116}
117
118/// Library subscription resource owned by a single connection process.
119#[derive(Debug)]
120pub struct ConnectionSubscription {
121    id: u64,
122    /// Channel this subscription was opened on, carried for DIAGNOSTICS only —
123    /// the shed warning has to name the channel or an operator reading it learns
124    /// that something was shed without learning what. Empty when unset (the
125    /// scheduler-free unit fixtures, which construct through `new` and never log).
126    channel: String,
127    /// Client-chosen application stream the server delivers this subscription's
128    /// messages on (echoed on `SubscribeAck`, carried on every `Deliver`). Set by
129    /// the connection process from the `Subscribe` frame before the subscription is
130    /// stored; `0` only while momentarily unset during construction.
131    stream_id: u32,
132    selected_schema: ProtocolSchemaId,
133    resource: Box<dyn SubscriptionResource>,
134    issued: super::state::IssuedDeliveries,
135}
136
137impl ConnectionSubscription {
138    /// Creates an owned subscription resource for one connection process.
139    #[must_use]
140    pub fn new(
141        id: u64,
142        selected_schema: ProtocolSchemaId,
143        resource: Box<dyn SubscriptionResource>,
144    ) -> Self {
145        Self {
146            id,
147            channel: String::new(),
148            stream_id: 0,
149            selected_schema,
150            resource,
151            issued: super::state::IssuedDeliveries::new(0),
152        }
153    }
154
155    /// Records the channel this subscription was opened on (diagnostics).
156    ///
157    /// Additive setter rather than a fourth `new` parameter: `new` is public API
158    /// and its existing callers keep compiling byte-for-byte.
159    pub(super) fn set_channel(&mut self, channel: String) {
160        self.channel = channel;
161    }
162
163    /// The channel this subscription was opened on, or `""` when unset.
164    #[must_use]
165    pub(super) fn channel(&self) -> &str {
166        &self.channel
167    }
168
169    /// Returns the protocol subscription id.
170    #[must_use]
171    pub const fn id(&self) -> u64 {
172        self.id
173    }
174
175    /// Records the client-chosen delivery stream id for this subscription.
176    pub(super) const fn set_stream_id(&mut self, stream_id: u32) {
177        self.stream_id = stream_id;
178    }
179
180    /// Returns the client-chosen application stream id deliveries ride on.
181    #[must_use]
182    pub(super) const fn stream_id(&self) -> u32 {
183        self.stream_id
184    }
185
186    pub(super) fn set_pressure_ledger_depth(&mut self, limit: usize) {
187        self.issued = super::state::IssuedDeliveries::new(limit);
188    }
189
190    pub(super) fn issued_delivery(&mut self, sequence: u64) {
191        self.issued.record(sequence);
192    }
193
194    pub(super) fn match_pressure(&mut self, reference: &str, retain: bool) -> bool {
195        self.issued.matches(reference, retain)
196    }
197
198    /// Returns the schema selected for this subscription stream.
199    #[must_use]
200    pub const fn selected_schema(&self) -> ProtocolSchemaId {
201        self.selected_schema
202    }
203
204    /// The library subscriber's pid, when the subscription has one.
205    #[must_use]
206    pub fn subscriber_pid(&self) -> Option<u64> {
207        self.resource.subscriber_pid()
208    }
209
210    /// Attempts to pull the next delivered envelope without blocking.
211    pub(crate) fn try_next(&mut self) -> Option<liminal::envelope::Envelope> {
212        self.resource.try_next()
213    }
214
215    pub(crate) fn has_pending(&self) -> bool {
216        self.resource.has_pending()
217    }
218
219    /// Whether this subscription has been shed by an inbox overflow (§5).
220    pub(crate) fn is_overflowed(&self) -> bool {
221        self.resource.is_overflowed()
222    }
223
224    pub(super) fn unsubscribe(self) -> Result<(), ServerError> {
225        self.resource.unsubscribe()
226    }
227
228    pub(crate) fn try_next_positioned(
229        &mut self,
230    ) -> Result<Option<(liminal::envelope::Envelope, Option<u64>)>, ServerError> {
231        self.resource.try_next_positioned()
232    }
233
234    pub(crate) fn acknowledge_position(&self, position: u64) -> Result<(), ServerError> {
235        self.resource.acknowledge_position(position)
236    }
237
238    pub(crate) fn consumer_position(&self) -> Result<Option<u64>, ServerError> {
239        self.resource.consumer_position()
240    }
241}
242
243/// Outcome of a server publish.
244///
245/// Carries the assigned message id plus a genuine delivery ack (`delivered` = the
246/// message was accepted by at least one live subscriber on this publish, after any
247/// dedup-on-delivery suppression).
248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
249pub struct PublishOutcome {
250    /// Transport-scoped message id assigned to the accepted publish.
251    ///
252    /// TRANSPORT-SCOPED, NOT A DURABLE POSITION (G1,
253    /// `docs/design/wire-durability-gaps-20260831.md`): this is a
254    /// process-local counter (`next_message_id`, reset to 1 on every
255    /// construction), so it restarts from 1 with the server and two servers
256    /// mint the same ids. It is monotonic only within one service instance's
257    /// lifetime and identifies the publish on THIS connection's wire, nothing
258    /// more. The durable position of a publish on a durable channel is the
259    /// store-assigned sequence the library now surfaces as
260    /// `ChannelDelivery::durable_position`; putting that on the wire ack is
261    /// the A1 resume lane's frame decision, not a reinterpretation of this
262    /// field.
263    pub message_id: u64,
264    /// Whether the message was genuinely delivered to a subscriber. `false` means
265    /// the publish was accepted but reached no subscriber (empty channel) or was
266    /// a duplicate suppressed by dedup-on-delivery.
267    pub delivered: bool,
268    /// Store-assigned position of the row this outcome answers for, when the
269    /// publish is known to have one. Unlike `message_id` this IS a durable
270    /// position; the connection process puts it on the wire ack only for a
271    /// connection that negotiated `PUBLISH_ACK_POSITION_VERSION`.
272    pub durable_position: Option<u64>,
273    /// Whom the publish reached, the console tap's word for it (SRV-012
274    /// R10): a subscriber, no subscriber, only subscribers that refused it,
275    /// or nobody because dedup suppressed it.
276    pub reach: Reach,
277}
278
279/// Whom a publish reached (SRV-012 R10).
280#[derive(Clone, Copy, Debug, PartialEq, Eq)]
281pub enum Reach {
282    /// At least one live subscriber took it.
283    Delivered,
284    /// The channel had no matching subscriber.
285    NoSubscriber,
286    /// Every matching subscriber refused it.
287    NoneAccepted,
288    /// A dedup-suppressed re-publish, sent to nobody.
289    Suppressed,
290}
291
292impl Reach {
293    /// The reach of a library delivery.
294    #[must_use]
295    pub const fn of(delivery: &ChannelDelivery) -> Self {
296        if delivery.is_delivered() {
297            Self::Delivered
298        } else if delivery.matched_count() == 0 {
299            Self::NoSubscriber
300        } else {
301            Self::NoneAccepted
302        }
303    }
304}
305
306/// Which channel operation is asking the roster for admission.
307///
308/// `Copy`, so the hot path allocates nothing to ask. The contrast is
309/// [`ServerError::UnsupportedOperation`]'s owned `operation: String`: that one is
310/// built on a refusal path and pays for its allocation once per refusal, while
311/// this one is consulted on every publish and every subscribe frame that reaches
312/// the connection process.
313#[derive(Clone, Copy, Debug, PartialEq, Eq)]
314pub enum ChannelOperation {
315    /// A `Publish` frame is asking.
316    Publish,
317    /// A `Subscribe` frame is asking.
318    Subscribe,
319}
320
321/// Operations that adapt wire frames to liminal library calls.
322pub trait ConnectionServices: std::fmt::Debug + Send + Sync {
323    /// Returns the complete participant service installed on this adapter.
324    ///
325    /// `None` keeps participant capability disabled even when the adapter owns a
326    /// durable store for unrelated channel traffic. The returned token is
327    /// server-sealed and atomically carries declared semantics plus durability,
328    /// making a handler-without-store activation impossible by construction.
329    fn participant_service(&self) -> Option<InstalledParticipantService> {
330        None
331    }
332
333    /// Delegates a publish request to the liminal library.
334    ///
335    /// `connection` is the publishing connection's id (its process pid, as
336    /// `apply_frame` knows it), carried as the publish origin so the console
337    /// tap attributes the message to it (SRV-012). `idempotency_key`, when `Some`, drives dedup-on-delivery: a re-publish with
338    /// the same key is delivered to subscribers at most once. The returned
339    /// [`PublishOutcome`] carries the genuine delivery ack.
340    ///
341    /// # Errors
342    /// Returns [`ServerError`] when the liminal publish operation fails.
343    fn publish(
344        &self,
345        connection: u64,
346        channel: &str,
347        envelope: &MessageEnvelope,
348        idempotency_key: Option<&str>,
349    ) -> Result<PublishOutcome, ServerError>;
350
351    /// Delegates a subscribe request to the liminal library.
352    ///
353    /// `install`, when `Some`, carries the connection's §5 shared inbox byte
354    /// budget, per-inbox fairness cap, and R3 wake notifier. The implementation
355    /// MUST install it on the subscription's inbox BEFORE the registration is
356    /// published to the channel actor (i.e. before any envelope can be
357    /// delivered), so no envelope is ever admitted uncharged, past the depth
358    /// cap, or without a wake. Implementations with no real inbox (test
359    /// stand-ins, capability-scoped profiles that refuse subscribe) may ignore
360    /// it.
361    ///
362    /// # Errors
363    /// Returns [`ServerError`] when the liminal subscribe operation fails.
364    fn subscribe(
365        &self,
366        channel: &str,
367        accepted_schemas: &[ProtocolSchemaId],
368        install: Option<liminal::channel::InboxInstall>,
369    ) -> Result<ConnectionSubscription, ServerError>;
370
371    /// Delegates unsubscribe to the liminal library.
372    ///
373    /// # Errors
374    /// Returns [`ServerError`] when the liminal unsubscribe operation fails.
375    fn unsubscribe(&self, subscription: ConnectionSubscription) -> Result<(), ServerError>;
376
377    /// Delegates conversation open to the liminal library.
378    ///
379    /// # Errors
380    /// Returns [`ServerError`] when the liminal conversation open operation fails.
381    fn open_conversation(
382        &self,
383        conversation_id: u64,
384        subject: &str,
385    ) -> Result<ConnectionConversation, ServerError>;
386
387    /// Delegates a conversation message to the liminal library.
388    ///
389    /// # Errors
390    /// Returns [`ServerError`] when the liminal conversation message operation fails.
391    fn conversation_message(
392        &self,
393        conversation: &ConnectionConversation,
394        envelope: &MessageEnvelope,
395        op_id: Option<u64>,
396    ) -> Result<(), ServerError>;
397
398    /// Delegates conversation close to the liminal library.
399    ///
400    /// # Errors
401    /// Returns [`ServerError`] when the liminal conversation close operation fails.
402    fn close_conversation(&self, conversation: ConnectionConversation) -> Result<(), ServerError>;
403
404    /// Flushes the shared durable store after channel publications have settled.
405    ///
406    /// # Errors
407    /// Returns [`ServerError`] when the shared store flush operation fails.
408    fn flush_durable_state(&self) -> Result<(), ServerError>;
409
410    /// Stops the participant warm and records the clean stop's capacity
411    /// snapshot (DUR-020 R3, R5). Called once, by the shutdown sequence,
412    /// before the final flush; an adapter with no participant does nothing.
413    fn stop_participant_warm(&self) {}
414
415    /// Joins background work that can still write to the store.
416    ///
417    /// # Errors
418    /// Returns a named shutdown failure when an owned worker cannot be joined.
419    fn finish_background_store_work(&self) -> Result<(), ServerError> {
420        Ok(())
421    }
422
423    /// Stops store-owning service workers after the final flush.
424    ///
425    /// # Errors
426    /// Returns a named shutdown failure when a service worker cannot be joined.
427    fn release_store_holders(&self) -> Result<(), ServerError> {
428        Ok(())
429    }
430
431    /// The listener is bound: whatever the start reads that no admission
432    /// needs before it is judged runs from here, behind the listening door.
433    /// An adapter with nothing to read does nothing. Answers the thread it
434    /// started, which the listener leaves to run and a test may join.
435    fn listening(self: Arc<Self>) -> Option<std::thread::JoinHandle<()>> {
436        None
437    }
438
439    /// Whether this adapter backs ordinary channel and conversation operations.
440    ///
441    /// The default is `true` — the full-service adapter serves publish, subscribe,
442    /// and conversation frames, so full mode is byte-for-byte unchanged. The
443    /// capability-scoped worker front door overrides this to `false`, letting
444    /// [`super::apply`] reject the channel/conversation frames it short-circuits on
445    /// empty connection state (`Unsubscribe`, `ConversationMessage`,
446    /// `ConversationClose`) with a typed error frame instead of silently swallowing
447    /// an operation for a resource that could never have been created in this
448    /// profile. Frames that always reach a service method (`Publish`, `Subscribe`,
449    /// `ConversationOpen`) are rejected by the front door's own method bodies and do
450    /// not consult this flag.
451    fn supports_channel_operations(&self) -> bool {
452        true
453    }
454
455    /// Whether `channel` admits `operation` right now.
456    ///
457    /// Consulted by the connection process BEFORE the operation is delegated, so
458    /// a roster refusal is typed at the moment of the decision, by the component
459    /// that made it, from the value it decided on. The alternative — classifying
460    /// an opaque failure afterwards by re-reading the roster in the error arm —
461    /// cannot tell "the roster refused this" from "something else failed while
462    /// the roster happened to change", and would put a confident wrong cause on
463    /// the wire.
464    ///
465    /// It does NOT replace the adapter's own inner check. Admission here is the
466    /// caller's guard; a service method is public and callable without a frame,
467    /// so it keeps its own.
468    ///
469    /// The default ADMITS. An adapter with no roster has nothing to say here and
470    /// its refusals travel as service errors exactly as they do today; only the
471    /// roster-owning adapter overrides this. A default body is also what keeps
472    /// this addition inside "minor": a method added without one breaks every
473    /// downstream implementor of a public trait.
474    ///
475    /// # Errors
476    /// Returns [`ChannelAccessError`] when the roster refuses the operation.
477    fn admit_channel(
478        &self,
479        operation: ChannelOperation,
480        channel: &str,
481    ) -> Result<(), ChannelAccessError> {
482        let _ = (operation, channel);
483        Ok(())
484    }
485}
486
487/// Default adapter from server wire frames to liminal channel/conversation APIs.
488#[derive(Debug)]
489pub struct LiminalConnectionServices {
490    /// The channel roster, keyed by channel name.
491    ///
492    /// Behind an [`RwLock`] over `Arc` values so a reader clones one pointer out
493    /// under the guard and works with it after the guard is released: no roster
494    /// read is ever held across a library call, and an entry handed out can
495    /// outlive a concurrent mutation of the map itself.
496    channels: RwLock<HashMap<String, Arc<ConfiguredChannel>>>,
497    export_subscriptions:
498        Mutex<Vec<std::sync::Weak<crate::link_bridge::subscriptions::ExportSubscriptions>>>,
499    /// The operator-declared bound on RUNTIME-registered channels
500    /// (`limits.max_channels`), carried verbatim from config.
501    ///
502    /// `None` is "the operator declared no bound", which refuses every runtime
503    /// registration rather than admitting an unbounded one — the roster's
504    /// aggregate size is otherwise unbounded the moment a registration API
505    /// exists. Boot-configured channels are never counted against it: they are
506    /// the bound the operator already wrote, in the file they wrote it in.
507    max_channels: Option<usize>,
508    cluster: ChannelCluster,
509    durable_store: Arc<dyn DurableStore>,
510    head_checkpoint_stop: Arc<liminal::durability::head_checkpoint_stop::HeadCheckpointStop>,
511    /// Reads the counters of every store these services built: the durable
512    /// store and, when it is a separate store, the ephemeral dedup store.
513    store_counts: StoreCountsHandle,
514    ephemeral_sweep: Option<EphemeralSweepCounts>,
515    signals: ServerSignals,
516    /// Complete participant service, installed only when semantic lifecycle
517    /// handling and its durable aggregate store are both ready.
518    participant_service: Option<InstalledParticipantService>,
519    /// The production handler's refused-load record, captured at construction.
520    ///
521    /// [`InstalledParticipantService`] holds its handler behind
522    /// `dyn ParticipantSemanticHandler`, so the concrete production handler is
523    /// no longer reachable once it is installed; the record is taken here, in
524    /// the one place the concrete handler exists, and carried out to the
525    /// startup path that publishes it onto the health endpoint. `None` is "no
526    /// participant is configured", which the operator surface reports as such
527    /// rather than as an empty refusal set.
528    unloadable_conversations: Option<UnloadableConversationRecord>,
529    /// The production handler as the A7 operator credential-re-issue authority
530    /// (§0.18), captured at construction for exactly the reason above: the
531    /// concrete handler is unreachable once erased behind
532    /// `dyn ParticipantSemanticHandler`, so the operator surface's handle on it
533    /// has to be taken in the one place it still exists. `None` is "no
534    /// participant is configured", which the route reports as such.
535    credential_reissuer: Option<Arc<dyn OperatorCredentialReissuer>>,
536    /// The participant warm's stop handle (DUR-020 R4, R5), captured at
537    /// construction for the same reason as the two above. The shutdown flush
538    /// stops the warm first, so no conversation begins restoring after the
539    /// server has begun to stop. `None` is "no participant is configured".
540    participant_warm: Option<ParticipantWarm>,
541    /// Dedup-on-delivery cache for publishes to **durable** channels. Keyed by
542    /// the per-message idempotency key carried on the publish frame; a duplicate
543    /// key is suppressed before fan-out so a subscriber receives it at most once.
544    ///
545    /// The namespace is stable across restarts by design (A1 §5): on a durable
546    /// channel the MESSAGE survives a crash too, so a surviving receipt is
547    /// truthful.
548    dedup: DedupCache,
549    /// Dedup-on-delivery cache for publishes to **ephemeral** channels
550    /// (A1 §5, graft §0.2 — the panel's sharpest crash hole).
551    ///
552    /// Identical machinery, different namespace: this one carries a per-boot
553    /// incarnation id, so a receipt and the message it vouches for share a
554    /// lifetime. Without it, `ephemeral + keyed publish -> Defer (buffered in
555    /// memory) -> complete_receipt (persisted) -> crash before the consumer
556    /// pops` loses the message while the receipt survives to suppress the
557    /// producer's retry — silent permanent loss of something the producer was
558    /// told was admitted. With it, the post-crash retry re-claims and
559    /// re-delivers, which is exactly ephemeral's at-least-once contract.
560    ///
561    /// This changes no dedup API. It changes only the namespace string the
562    /// server composes.
563    dedup_ephemeral: DedupCache,
564    /// Durable-channel idempotency keys whose publish outcome is not yet
565    /// known, held until their row is looked up by its key. A cache of the
566    /// durable in-flight claims, rebuilt from them when the services start.
567    held_keys: unresolved::HeldKeys,
568    /// The store history whose in-flight claims are still to be resolved:
569    /// some until the resolve has run, behind the listening door or at the
570    /// first keyed durable admission, whichever comes first (`unresolved_start.rs`).
571    claims_start: Mutex<Option<StoreHistory>>,
572    conversation_supervisor: Arc<ConversationSupervisor>,
573    /// Registered custom conversation responders, keyed by conversation subject.
574    ///
575    /// When a conversation is opened (`open_conversation`), the subject is looked
576    /// up here: a registered [`ParticipantBehaviour`] becomes the conversation's
577    /// participant; with no registration the local route refuses by name.
578    /// This is the seam aion #13 plugs a remote worker responder into. Interior
579    /// mutability is required because the services are shared behind `&self`.
580    responders: Mutex<ResponderRegistry>,
581    next_message_id: AtomicU64,
582    next_subscription_id: AtomicU64,
583    /// SRV-012: the console's held topology and tap.
584    console: Arc<crate::console::ConsoleHub>,
585}
586
587impl LiminalConnectionServices {
588    /// Builds library-backed services from validated server configuration.
589    ///
590    /// Durable-mode channels are backed by a shared haematite event store so
591    /// their publishes are persisted and survive the graceful-shutdown flush;
592    /// ephemeral channels carry no store.
593    ///
594    /// Full-only: a config selecting the worker-front-door profile is rejected at
595    /// entry (before any store is built), so this constructor can never build full
596    /// services for a profile that forbids them. Profile-aware callers go through
597    /// [`build_connection_services`] instead.
598    ///
599    /// # Errors
600    /// Returns [`ServerError`] when the config selects a non-full profile or a
601    /// configured channel cannot be initialized.
602    pub fn from_config(config: &ServerConfig) -> Result<Self, ServerError> {
603        require_full_profile(config)?;
604        let path = required_persistence_path(config)?;
605        let history = StoreHistory::of(path);
606        let subsystems = RootedProductionSubsystems::new(path)?;
607        let store = subsystems.durable_store(path)?;
608        Self::from_config_with_store_via(config, store, history, &subsystems)
609    }
610
611    /// Builds services over a caller-provided durable store.
612    ///
613    /// Used by tests that need to inspect persisted state through the same store
614    /// handle the durable channels write to.
615    ///
616    /// Full-only: rejects a worker-front-door profile at entry, exactly like
617    /// [`Self::from_config`].
618    ///
619    /// # Errors
620    /// Returns [`ServerError`] when the config selects a non-full profile or a
621    /// configured channel cannot be initialized.
622    pub fn from_config_with_store(
623        config: &ServerConfig,
624        durable_store: Arc<dyn DurableStore>,
625    ) -> Result<Self, ServerError> {
626        require_full_profile(config)?;
627        #[cfg(any(test, feature = "test-support"))]
628        if config.persistence_path.is_none() {
629            return Self::from_config_with_store_via(
630                config,
631                durable_store,
632                StoreHistory::Recorded,
633                &ProductionSubsystems,
634            );
635        }
636        let subsystems = RootedProductionSubsystems::new(required_persistence_path(config)?)?;
637        Self::from_config_with_store_via(config, durable_store, StoreHistory::Recorded, &subsystems)
638    }
639
640    /// [`Self::from_config_with_store`] with the subsystem factory injected.
641    ///
642    /// The channel supervisor and conversation supervisor are constructed ONLY
643    /// through `subsystems` — there is no direct constructor call in this body —
644    /// so a factory that records as a side effect of constructing cannot have its
645    /// recording omitted (§9 D2 seam census, record-by-construction). No profile
646    /// check here: the caller (the public wrapper or the profile dispatch in
647    /// [`build_connection_services`]) has already established the full profile.
648    /// `history` says whether `durable_store` can already hold in-flight
649    /// claims for the services to look up before they judge a publish.
650    fn from_config_with_store_via(
651        config: &ServerConfig,
652        durable_store: Arc<dyn DurableStore>,
653        history: StoreHistory,
654        subsystems: &dyn SubsystemFactory,
655    ) -> Result<Self, ServerError> {
656        let head_checkpoint_stop = Arc::new(
657            liminal::durability::head_checkpoint_stop::HeadCheckpointStop::new(durable_store),
658        );
659        let durable_store: Arc<dyn DurableStore> = head_checkpoint_stop.clone();
660        // Build ONE shared channel supervisor for the whole server. When a
661        // [cluster] section is present it is distribution-enabled, so every
662        // channel actor and subscriber shares the clustered scheduler the cluster
663        // attaches its process-group transport to (SRV-005, Constraint B).
664        let cluster = subsystems.channel_cluster(config.cluster.as_ref())?;
665        let mut channels = HashMap::new();
666        for channel in &config.channels {
667            // Resolve the channel's real JSON Schema (loaded from `schema_ref`
668            // during config validation) or the permissive empty schema when the
669            // channel declared none. The protocol schema id advertised at
670            // subscribe time is derived from the SAME schema bytes so an SDK
671            // deriving ids from schema bytes converges on it.
672            let resolved = resolve_channel_schema(channel);
673            let configured = build_configured_channel(
674                &channel.name,
675                resolved,
676                channel.durable,
677                ChannelOrigin::BootConfigured,
678                &durable_store,
679                cluster.supervisor(),
680            )
681            .map_err(|error| ServerError::ConfigValidation {
682                message: error.boot_message(&channel.name),
683            })?;
684            channels.insert(channel.name.clone(), Arc::new(configured));
685        }
686        let conversation_supervisor = subsystems.conversation_supervisor()?;
687        let dedup = history.delivery_dedup(Arc::clone(&durable_store));
688        // A1 §5: the ephemeral namespace is scoped to THIS incarnation, so an
689        // ephemeral receipt cannot outlive the in-memory message it vouches for.
690        // No earlier process used it, so it starts empty.
691        let ephemeral_store = ephemeral_dedup_store(&durable_store, history, subsystems);
692        let store_counts = combined_store_counts(&durable_store, &ephemeral_store);
693        let dedup_ephemeral = DedupCache::new_empty(ephemeral_store, ephemeral_dedup_namespace());
694        // Production participant activation (LP gap closure, Part B): the
695        // deployment's [participant] section installs the ONE production
696        // semantic handler, sealed together with the same durable store the
697        // conversation logs live in, under the configured wire-frame limit.
698        // No section, no service — the capability bit stays off and the
699        // connection path is byte-identical to the pre-activation build.
700        let signals = ServerSignals::default();
701        let installed_participant = config
702            .participant
703            .as_ref()
704            .map(|participant| install_participant(&durable_store, participant, &signals))
705            .transpose()?;
706        let device = config
707            .cluster
708            .as_ref()
709            .map_or("local", |cluster| cluster.node_name.as_str());
710        let console = Arc::new(console_hub(
711            device,
712            &channels,
713            config.persistence_path.is_some(),
714            config.auth.as_ref().and_then(|auth| auth.lys.as_ref()),
715        )?);
716        signals.install_console(Arc::clone(&console))?;
717        cluster.observer().set_console(Arc::clone(&console))?;
718        let (participant_service, unloadable_conversations, credential_reissuer, participant_warm) =
719            installed_participant.map_or((None, None, None, None), |installed| {
720                (
721                    Some(installed.service),
722                    Some(installed.unloadable_conversations),
723                    Some(installed.credential_reissuer),
724                    Some(installed.warm),
725                )
726            });
727        let services = Self {
728            channels: RwLock::new(channels),
729            export_subscriptions: Mutex::new(Vec::new()),
730            max_channels: config.limits.max_channels,
731            cluster,
732            durable_store,
733            head_checkpoint_stop,
734            store_counts,
735            ephemeral_sweep: subsystems.ephemeral_sweep_counts(),
736            signals,
737            participant_service,
738            unloadable_conversations,
739            credential_reissuer,
740            participant_warm,
741            dedup,
742            dedup_ephemeral,
743            held_keys: unresolved::HeldKeys::default(),
744            // Every durable in-flight claim is looked up by its key before the
745            // services judge a keyed durable publish: behind the listening
746            // door, or by that publish itself when it comes first.
747            claims_start: Mutex::new(Some(history)),
748            conversation_supervisor,
749            responders: Mutex::new(HashMap::new()),
750            next_message_id: AtomicU64::new(1),
751            next_subscription_id: AtomicU64::new(1),
752            console,
753        };
754        Ok(services)
755    }
756
757    /// Builds services with no configured channels.
758    ///
759    /// # Errors
760    /// Returns [`ServerError`] when the conversation supervisor scheduler cannot start.
761    #[cfg(any(test, feature = "test-support"))]
762    pub fn empty() -> Result<Self, ServerError> {
763        let conversation_supervisor = ProductionSubsystems.conversation_supervisor()?;
764        let store =
765            open_ephemeral(DEFAULT_SHARD_COUNT).map_err(|error| ServerError::ConfigValidation {
766                message: format!("failed to open test durable store: {error}"),
767            })?;
768        let head_checkpoint_stop = Arc::new(
769            liminal::durability::head_checkpoint_stop::HeadCheckpointStop::new(Arc::new(
770                CountingStore::new(store),
771            )),
772        );
773        let durable_store: Arc<dyn DurableStore> = head_checkpoint_stop.clone();
774        let store_counts = combined_store_counts(&durable_store, &durable_store);
775        let dedup = StoreHistory::Fresh.delivery_dedup(Arc::clone(&durable_store));
776        // A1 §5: the ephemeral namespace is scoped to THIS incarnation, so an
777        // ephemeral receipt cannot outlive the in-memory message it vouches for.
778        let dedup_ephemeral =
779            DedupCache::new_empty(Arc::clone(&durable_store), ephemeral_dedup_namespace());
780        let signals = ServerSignals::default();
781        let console = Arc::new(console_hub("local", &HashMap::new(), false, None)?);
782        signals.install_console(Arc::clone(&console))?;
783        let cluster = build_channel_cluster(None)?;
784        cluster.observer().set_console(Arc::clone(&console))?;
785        Ok(Self {
786            channels: RwLock::new(HashMap::new()),
787            export_subscriptions: Mutex::new(Vec::new()),
788            // No config, so no declared bound: this builder serves tests and
789            // callers with no channels at all, and a registration against it
790            // refuses `CapNotConfigured` exactly as an undeclared deployment's
791            // would.
792            max_channels: None,
793            cluster,
794            durable_store,
795            head_checkpoint_stop,
796            store_counts,
797            ephemeral_sweep: None,
798            signals,
799            participant_service: None,
800            unloadable_conversations: None,
801            credential_reissuer: None,
802            participant_warm: None,
803            dedup,
804            dedup_ephemeral,
805            held_keys: unresolved::HeldKeys::default(),
806            claims_start: Mutex::new(None),
807            conversation_supervisor,
808            responders: Mutex::new(HashMap::new()),
809            next_message_id: AtomicU64::new(1),
810            next_subscription_id: AtomicU64::new(1),
811            console,
812        })
813    }
814
815    /// The retained counts from the root's one startup sweep.
816    #[must_use]
817    pub const fn ephemeral_sweep_counts(&self) -> Option<EphemeralSweepCounts> {
818        self.ephemeral_sweep
819    }
820
821    /// The shared channel supervisor + cluster resolver backing this service.
822    ///
823    /// The server runtime uses this to attach the cluster to the channel
824    /// supervisor's clustered scheduler (SRV-005).
825    #[must_use]
826    pub const fn channel_cluster(&self) -> &ChannelCluster {
827        &self.cluster
828    }
829
830    /// Returns the shared durable store backing this service's durable channels.
831    #[must_use]
832    pub fn durable_store(&self) -> Arc<dyn DurableStore> {
833        Arc::clone(&self.durable_store)
834    }
835
836    /// The console's held topology and tap (SRV-012).
837    #[must_use]
838    pub fn console(&self) -> &crate::console::ConsoleHub {
839        &self.console
840    }
841
842    /// Applies a topology change; a poisoned model is counted by the hub
843    /// (`topology_emit_failures`) and logged, never recovered.
844    fn console_changed(&self, change: crate::console::TopologyChange) {
845        self.signals.topology(change);
846    }
847
848    /// Conversation subjects with a registered responder, sorted. Read from
849    /// the in-memory registry; touches no store.
850    ///
851    /// # Errors
852    /// Returns [`ServerError`] when the responder registry lock is poisoned.
853    pub fn responder_subjects(&self) -> Result<Vec<String>, ServerError> {
854        let responders = self.lock_responders()?;
855        let mut subjects: Vec<String> = responders.keys().cloned().collect();
856        drop(responders);
857        subjects.sort();
858        Ok(subjects)
859    }
860
861    /// Restores every participant conversation the warm has not yet reached,
862    /// on the caller's thread, and answers how many refused (DUR-020 R4).
863    /// The store-count journeys call it so their windows do not race the
864    /// warm thread; the server itself never waits for the warm.
865    ///
866    /// # Errors
867    /// Returns [`ServerError`] when the pending-restore lock is poisoned.
868    pub fn finish_participant_warm(&self) -> Result<usize, ServerError> {
869        self.participant_warm.as_ref().map_or(Ok(0), |warm| {
870            warm.finish()
871                .map_err(|error| ServerError::ParticipantStartupRestore {
872                    message: error.to_string(),
873                })
874        })
875    }
876
877    /// Reads the store work these services have done since they were built,
878    /// across every store they built.
879    ///
880    /// Empty (every snapshot zero) when the durable store was supplied by the
881    /// caller and does not count.
882    #[must_use]
883    pub fn store_counts(&self) -> StoreCountsHandle {
884        self.store_counts.clone()
885    }
886
887    /// The production participant handler's refused-load record, when a
888    /// participant is configured.
889    ///
890    /// The server's startup path publishes this onto the health endpoint so
891    /// `GET /unloadable-conversations` answers from the same record the
892    /// handler writes. `None` means no participant is configured at all, which
893    /// the surface reports as a distinct state from "nothing was refused".
894    #[must_use]
895    pub fn unloadable_conversation_record(&self) -> Option<UnloadableConversationRecord> {
896        self.unloadable_conversations.clone()
897    }
898
899    /// The A7 operator credential-re-issue authority, when a participant is
900    /// configured (§0.18).
901    ///
902    /// The startup path publishes this onto the health endpoint so
903    /// `POST /operator/credential-reissue` reaches the SAME serialized
904    /// participant-state point every wire operation does. `None` means no
905    /// participant is configured, which the route reports as a distinct state
906    /// from an unknown identity.
907    #[must_use]
908    pub fn credential_reissuer(&self) -> Option<Arc<dyn OperatorCredentialReissuer>> {
909        self.credential_reissuer.clone()
910    }
911
912    /// Installs a complete participant bundle in full-service supervisor tests.
913    ///
914    /// Production full services intentionally stay disabled until a concrete
915    /// lifecycle handler exists. This consuming test builder exercises the real
916    /// supervisor activation path without allowing an already-shared adapter to
917    /// change capability posture.
918    #[cfg(test)]
919    #[must_use]
920    pub(crate) fn with_participant_service(
921        mut self,
922        participant_service: InstalledParticipantService,
923    ) -> Self {
924        self.participant_service = Some(participant_service);
925        self
926    }
927
928    /// Returns the conversation supervisor backing supervised conversations.
929    ///
930    /// Tests use this to reach the underlying beamr scheduler so they can spawn
931    /// or terminate participant processes and exercise crash detection.
932    #[must_use]
933    pub fn conversation_supervisor(&self) -> Arc<ConversationSupervisor> {
934        Arc::clone(&self.conversation_supervisor)
935    }
936
937    /// Registers a conversation responder for a routing `subject`.
938    ///
939    /// A later conversation on this exact subject runs the registered behaviour
940    /// as a supervised linked participant. A registration replaces the previous
941    /// behaviour and returns it to the caller.
942    ///
943    /// # Errors
944    /// Returns [`ServerError`] when the responder registry lock is poisoned.
945    pub fn register_responder(
946        &self,
947        subject: impl Into<String>,
948        behaviour: Arc<dyn ParticipantBehaviour>,
949    ) -> Result<Option<Arc<dyn ParticipantBehaviour>>, ServerError> {
950        let subject = subject.into();
951        let mut responders = self.lock_responders()?;
952        let previous = responders.insert(subject.clone(), behaviour);
953        drop(responders);
954        self.console_changed(crate::console::TopologyChange::Service {
955            subject,
956            present: true,
957        });
958        Ok(previous)
959    }
960
961    /// Removes the custom responder registered for `subject`, if any.
962    ///
963    /// After removal local calls on the subject are refused with `no_server`.
964    /// Returns the removed behaviour when one was registered.
965    ///
966    /// # Errors
967    /// Returns [`ServerError`] when the responder registry lock is poisoned.
968    pub fn unregister_responder(
969        &self,
970        subject: &str,
971    ) -> Result<Option<Arc<dyn ParticipantBehaviour>>, ServerError> {
972        let mut responders = self.lock_responders()?;
973        let removed = responders.remove(subject);
974        drop(responders);
975        if removed.is_some() {
976            self.console_changed(crate::console::TopologyChange::Service {
977                subject: subject.to_owned(),
978                present: false,
979            });
980        }
981        Ok(removed)
982    }
983
984    /// Locks the responder registry, mapping a poisoned lock to a [`ServerError`]
985    /// rather than panicking (the workspace denies `unwrap`/`expect`/`panic`).
986    fn lock_responders(&self) -> Result<std::sync::MutexGuard<'_, ResponderRegistry>, ServerError> {
987        self.responders
988            .lock()
989            .map_err(|_poisoned| ServerError::ListenerAccept {
990                message: "responder registry lock poisoned".to_owned(),
991            })
992    }
993
994    /// Takes the channel roster's read guard, mapping a poisoned lock to a
995    /// [`ServerError`] rather than panicking (the workspace denies
996    /// `unwrap`/`expect`/`panic`), exactly as [`Self::lock_responders`] does for
997    /// the responder registry.
998    ///
999    /// Every caller clones the `Arc` it needs out of the returned guard and drops
1000    /// the guard before doing anything else: the roster lock is never held across
1001    /// a call into the liminal library.
1002    fn read_channels(
1003        &self,
1004    ) -> Result<std::sync::RwLockReadGuard<'_, HashMap<String, Arc<ConfiguredChannel>>>, ServerError>
1005    {
1006        self.channels
1007            .read()
1008            .map_err(|_poisoned| ServerError::ListenerAccept {
1009                message: "channel roster lock poisoned".to_owned(),
1010            })
1011    }
1012
1013    /// Subscribes to a configured channel and returns the raw library
1014    /// subscription handle so a test can drain the subscriber inbox directly and
1015    /// observe exactly which messages reached a subscriber.
1016    #[cfg(test)]
1017    pub(in crate::server) fn channel_handle_for_test(
1018        &self,
1019        channel: &str,
1020    ) -> Result<ChannelHandle, ServerError> {
1021        self.read_channels()?
1022            .get(channel)
1023            .map(|configured| configured.handle.clone())
1024            .ok_or_else(|| ServerError::ListenerAccept {
1025                message: format!("channel '{channel}' is not configured"),
1026            })
1027    }
1028
1029    #[cfg(test)]
1030    pub(crate) fn subscribe_handle_for_test(
1031        &self,
1032        channel: &str,
1033    ) -> Result<liminal::channel::SubscriptionHandle, ServerError> {
1034        self.subscribe_handle_for_test_with_install(channel, None)
1035    }
1036
1037    /// [`Self::subscribe_handle_for_test`] carrying an explicit install, so a
1038    /// test can put a subscriber under a REAL §5 regime (byte budget, fairness
1039    /// cap) and observe what a publish the bus dropped does to the dedup
1040    /// receipt.
1041    #[cfg(test)]
1042    pub(crate) fn subscribe_handle_for_test_with_install(
1043        &self,
1044        channel: &str,
1045        install: Option<liminal::channel::InboxInstall>,
1046    ) -> Result<liminal::channel::SubscriptionHandle, ServerError> {
1047        let channels = self.read_channels()?;
1048        let configured =
1049            channels
1050                .get(channel)
1051                .map(Arc::clone)
1052                .ok_or_else(|| ServerError::ListenerAccept {
1053                    message: format!("channel '{channel}' is not configured"),
1054                })?;
1055        drop(channels);
1056        install
1057            .map_or_else(
1058                || configured.handle.subscribe(),
1059                |install| configured.handle.subscribe_with_install(install),
1060            )
1061            .map_err(|error| ServerError::ListenerAccept {
1062                message: format!("liminal subscribe failed for channel '{channel}': {error}"),
1063            })
1064    }
1065
1066    /// Claims the delivery right for an idempotency key.
1067    ///
1068    /// Returns `Ok(true)` when this is the first publish for the key (the caller
1069    /// may deliver), and `Ok(false)` when the key was already claimed/completed (a
1070    /// duplicate the caller must suppress). The dedup cache is driven synchronously
1071    /// over the in-memory haematite store via the durable bridge.
1072    fn claim_delivery(cache: &DedupCache, key: &str) -> Result<bool, ServerError> {
1073        let decision = block_on(cache.claim_or_get(key, dedup_timestamp_millis()))
1074            .map_err(|error| ServerError::ListenerAccept {
1075                message: format!("dedup bridge failed for key '{key}': {error}"),
1076            })?
1077            .map_err(|error| ServerError::ListenerAccept {
1078                message: format!("dedup claim failed for key '{key}': {error}"),
1079            })?;
1080        Ok(matches!(decision, DedupDecision::Claimed))
1081    }
1082
1083    /// The dedup namespace a publish to `channel` belongs in (A1 §5).
1084    ///
1085    /// Durability decides, because durability is what determines whether a
1086    /// receipt can outlive the message it vouches for. A durable channel's
1087    /// message survives a crash, so its receipt may; an ephemeral channel's
1088    /// does not, so its receipt must not.
1089    const fn dedup_for(&self, channel: &ConfiguredChannel) -> &DedupCache {
1090        match channel.handle.config().mode {
1091            ChannelMode::Durable => &self.dedup,
1092            ChannelMode::Ephemeral => &self.dedup_ephemeral,
1093        }
1094    }
1095
1096    /// Releases a dangling in-flight dedup claim after a failed delivery.
1097    ///
1098    /// Best-effort: a release failure cannot mask the original publish error, so
1099    /// this returns nothing and logs at `error` level instead of surfacing. It is
1100    /// never silent — the leak (a permanently suppressed key) must be observable.
1101    /// `release_claim` itself never clobbers a stored receipt, so calling it on the
1102    /// failure path is safe even if a concurrent completion raced ahead.
1103    fn release_claim(cache: &DedupCache, key: &str) {
1104        match block_on(cache.release_claim(key)) {
1105            Ok(Ok(())) => {}
1106            Ok(Err(error)) => {
1107                tracing::error!(
1108                    idempotency_key = key,
1109                    %error,
1110                    "failed to release dedup claim after publish failure; key may stay suppressed"
1111                );
1112            }
1113            Err(error) => {
1114                tracing::error!(
1115                    idempotency_key = key,
1116                    %error,
1117                    "dedup release bridge failed after publish failure; key may stay suppressed"
1118                );
1119            }
1120        }
1121    }
1122
1123    /// The next connection-level message id.
1124    fn next_message_id(&self) -> u64 {
1125        self.next_message_id.fetch_add(1, Ordering::Relaxed)
1126    }
1127
1128    /// The outcome of a publish the channel took, recorded as accepted with
1129    /// its genuine subscriber deliveries. The delivered count (0 for an empty
1130    /// channel) is the same signal the delivery ack is derived from.
1131    fn delivered(&self, delivery: &ChannelDelivery) -> PublishOutcome {
1132        crate::metrics::publish_accepted();
1133        let delivered_count = u64::try_from(delivery.delivered_count()).unwrap_or(u64::MAX);
1134        crate::metrics::deliveries_recorded(delivered_count);
1135        PublishOutcome {
1136            message_id: self.next_message_id(),
1137            delivered: delivery.is_delivered(),
1138            durable_position: delivery.durable_position(),
1139            reach: Reach::of(delivery),
1140        }
1141    }
1142}
1143
1144#[path = "deferred_store.rs"]
1145mod deferred_store;
1146#[path = "unresolved.rs"]
1147mod unresolved;
1148#[path = "unresolved_start.rs"]
1149mod unresolved_start;
1150#[cfg(test)]
1151#[path = "unresolved_start_tests.rs"]
1152mod unresolved_start_tests;
1153
1154use deferred_store::{DeferredStore, ThrowawayStoreOpener};
1155use unresolved_start::StoreHistory;
1156
1157/// The runtime channel-registration surface.
1158///
1159/// # Reachability: a test seam today, not a production capability
1160///
1161/// Production channels come exclusively from boot configuration; no production
1162/// code path calls `register_channel` (caller census at `c381f01`, G2 in
1163/// `docs/design/wire-durability-gaps-20260831.md` — the only callers are
1164/// `services_registry_tests.rs` and `apply_admission_tests.rs`). The method is
1165/// kept because its semantics — cap gating, typed identity refusals,
1166/// idempotent re-registration — are the contract a future runtime admission
1167/// path must satisfy, and the tests below pin that contract now. Wiring an
1168/// actual runtime admission route (an operator endpoint, a wire frame) is a
1169/// product decision that has NOT been taken; nothing here implies it. Reading
1170/// this method as an existing operator capability is the false-affordance
1171/// mistake this paragraph exists to prevent.
1172///
1173/// Inherent methods, not trait methods, and deliberately so: this is
1174/// authority-moving vocabulary that belongs to the ONE adapter owning a channel
1175/// roster. Putting `register`/`quiesce` on the public [`ConnectionServices`]
1176/// trait would break every external implementor and would hand
1177/// register/quiesce words to a profile that serves no channels at all.
1178///
1179/// The same seam as the existing runtime-mutation API on this type
1180/// (`register_responder`/`unregister_responder`): `&self`, interior mutability,
1181/// typed `Result`.
1182impl LiminalConnectionServices {
1183    /// Registers `spec` on the live roster.
1184    ///
1185    /// Test seam today: see the reachability note on this `impl` block — no
1186    /// production path reaches this method.
1187    ///
1188    /// Idempotent when an entry of that name already has an IDENTICAL
1189    /// configuration — mode, protocol schema id, and schema document, all three
1190    /// — and refuses typed, naming the first differing field, otherwise. An
1191    /// identical registration against a boot-configured entry answers
1192    /// [`Registered::AlreadyIdentical`] and leaves its origin alone: flipping it
1193    /// would make the entry lie about its restart fate and would move it into
1194    /// the counted population without a channel having been created.
1195    ///
1196    /// The cap is consulted first, in two steps with different reach. An absent
1197    /// `limits.max_channels` refuses EVERY call, identical or not, before the
1198    /// roster is read at all: a deployment that has declared no bound has not
1199    /// said what it admits, and unbounded-by-default is not a bound. A REACHED
1200    /// cap refuses only a call that would create an entry — it gates the insert,
1201    /// which is the population it bounds. An identical re-registration inserts
1202    /// nothing and so is answered `AlreadyIdentical` even at a full roster;
1203    /// refusing it would break idempotency at exactly the boundary a projector
1204    /// re-projecting its record crosses.
1205    ///
1206    /// # Errors
1207    /// Returns [`ChannelRegistryError`] when no cap is configured, the cap is
1208    /// reached, the name exists with a different configuration, the schema bytes
1209    /// do not parse or compile, durable initialization over the shared store
1210    /// fails, or the roster lock is poisoned.
1211    pub fn register_channel(
1212        &self,
1213        spec: &ChannelRegistration,
1214    ) -> Result<Registered, ChannelRegistryError> {
1215        // The cap is consulted before the roster is touched: an undeclared bound
1216        // refuses every runtime registration, identical or not, because the
1217        // deployment has not said what it admits.
1218        let Some(limit) = self.max_channels else {
1219            return Err(ChannelRegistryError::CapNotConfigured {
1220                cap: MAX_CHANNELS_KEY,
1221            });
1222        };
1223        // Schema resolution is pure (a JSON parse and a digest), so it runs with
1224        // no lock held and its result serves BOTH the identity comparison and
1225        // the construction below.
1226        let resolved = resolve_schema_bytes(spec.schema_bytes.as_deref()).map_err(|error| {
1227            ChannelRegistryError::SchemaRejected {
1228                name: spec.name.clone(),
1229                message: error.to_string(),
1230            }
1231        })?;
1232
1233        // Fast paths, under a READ lock: an already-identical registration
1234        // builds nothing, and a full roster refuses before paying for a
1235        // construction it would discard. Neither is the authority — the write
1236        // lock below re-decides both, because the roster can move in between.
1237        let (existing, registered_count) = {
1238            let channels = self.read_roster()?;
1239            let existing = channels.get(&spec.name).map(Arc::clone);
1240            let registered_count = runtime_registered_count(&channels);
1241            drop(channels);
1242            (existing, registered_count)
1243        };
1244        if let Some(existing) = existing {
1245            return compare_registration(&existing, spec, &resolved);
1246        }
1247        if registered_count >= limit {
1248            return Err(ChannelRegistryError::CapReached {
1249                cap: MAX_CHANNELS_KEY,
1250                limit,
1251            });
1252        }
1253
1254        // Construction runs with NO lock held. A durable channel recovers its
1255        // per-partition sequence counters from the store here, which is O(stream
1256        // length) in store reads; holding the roster lock across it would put a
1257        // slow store walk on every connection's publish and subscribe path.
1258        let configured = build_configured_channel(
1259            &spec.name,
1260            resolved,
1261            spec.durable,
1262            ChannelOrigin::RuntimeRegistered,
1263            &self.durable_store,
1264            self.cluster.supervisor(),
1265        )
1266        .map_err(|error| error.into_registry_error(&spec.name))?;
1267
1268        // The authoritative decision: identity, cap, and insert under ONE write
1269        // lock, so the count that admitted the entry and the insert that added
1270        // it cannot be separated by a concurrent registration.
1271        let mut exporters = self.export_subscriptions.lock().map_err(|_| {
1272            ChannelRegistryError::RosterUnavailable {
1273                message: "export subscription registry lock poisoned".to_owned(),
1274            }
1275        })?;
1276        exporters.retain(|exporter| exporter.strong_count() > 0);
1277        let mut channels = self.write_roster()?;
1278        let raced = channels.get(&spec.name).map(Arc::clone);
1279        let registered_count = runtime_registered_count(&channels);
1280        if raced.is_none() && registered_count < limit {
1281            let mut prepared = Vec::new();
1282            for exporter in exporters.iter().filter_map(std::sync::Weak::upgrade) {
1283                if let Some(binding) = exporter.binding(&spec.name) {
1284                    let refused = |error: ServerError| ChannelRegistryError::RosterUnavailable {
1285                        message: format!(
1286                            "export '{}' cannot subscribe to '{}': {error}",
1287                            exporter.link(),
1288                            spec.name
1289                        ),
1290                    };
1291                    let subscription = self
1292                        .build_subscription(
1293                            &spec.name,
1294                            &configured.handle,
1295                            configured.protocol_schema,
1296                            Some(binding.install),
1297                            Some(&binding.consumer),
1298                        )
1299                        .map_err(refused)?;
1300                    let subscription =
1301                        crate::link_bridge::subscriptions::PreparedSubscription::new(
1302                            binding.prefix,
1303                            spec.name.clone(),
1304                            subscription,
1305                        )
1306                        .map_err(refused)?;
1307                    prepared.push((exporter, subscription));
1308                }
1309            }
1310            // Bind every exporter before a publish can find the new roster entry.
1311            for (exporter, subscription) in prepared {
1312                exporter.attach(subscription);
1313            }
1314            channels.insert(spec.name.clone(), Arc::new(configured));
1315            drop(channels);
1316            drop(exporters);
1317            self.console_changed(crate::console::TopologyChange::Channel {
1318                name: spec.name.clone(),
1319                life: line_life(spec.durable),
1320                state: crate::console::records::LineState::Live,
1321            });
1322            return Ok(Registered::Created);
1323        }
1324        drop(channels);
1325        drop(exporters);
1326        if let Some(raced) = raced {
1327            // A racer registered this name while the channel above was being
1328            // built. The built entry is dropped unused: it owns no actor (the
1329            // actor is spawned lazily on first use) and its durable
1330            // construction only READ the store, so discarding it changes
1331            // nothing an observer could see.
1332            return compare_registration(&raced, spec, &schema_of(&configured));
1333        }
1334        Err(ChannelRegistryError::CapReached {
1335            cap: MAX_CHANNELS_KEY,
1336            limit,
1337        })
1338    }
1339
1340    /// Moves `name` from active to quiesced with a named `reason`. ONE-WAY.
1341    ///
1342    /// New publishes and new subscribes are refused afterwards, carrying the
1343    /// reason. Existing subscriptions are UNTOUCHED: nothing revokes a
1344    /// subscription handle, the actor's subscriber list is not walked, no EXIT
1345    /// is sent, and the channel actor keeps running. Quiesce is a roster-level
1346    /// admission decision, not an actor command.
1347    ///
1348    /// Re-quiescing under the IDENTICAL reason is `Ok(())`; a DIFFERENT reason
1349    /// refuses, carrying the reason already on record.
1350    ///
1351    /// The return does NOT mean "no new subscriber can appear". A subscribe that
1352    /// has already passed admission completes and gets its stream — the
1353    /// linearisation point is the admission read, not the subscribe's
1354    /// completion. A consumer that needs "nobody is attached" must observe
1355    /// attachment directly.
1356    ///
1357    /// # Errors
1358    /// Returns [`ChannelRegistryError`] when the name is not registered, is
1359    /// already quiesced under a different reason, or the roster lock is
1360    /// poisoned.
1361    pub fn quiesce_channel(
1362        &self,
1363        name: &str,
1364        reason: impl Into<String>,
1365    ) -> Result<(), ChannelRegistryError> {
1366        let reason = reason.into();
1367        // A READ lock: the state machine lives on the ENTRY, not in the map, so
1368        // the operation the design most wants to be safe never blocks a reader.
1369        let configured = {
1370            let channels = self.read_roster()?;
1371            channels.get(name).map(Arc::clone).ok_or_else(|| {
1372                ChannelRegistryError::NotRegistered {
1373                    name: name.to_owned(),
1374                }
1375            })?
1376        };
1377        configured.quiesce(name, &reason)?;
1378        self.console_changed(crate::console::TopologyChange::Channel {
1379            name: name.to_owned(),
1380            life: line_life(configured.durable),
1381            state: crate::console::records::LineState::Quiesced,
1382        });
1383        Ok(())
1384    }
1385
1386    /// Cheap typed probe: one roster read plus one atomic load.
1387    ///
1388    /// Touches no actor and therefore CANNOT spawn one. Every handle accessor
1389    /// that could answer a question about a channel's activity routes through
1390    /// the lazy-spawn path, so a probe built on one would materialise the actor
1391    /// of the idle channel it was asked about — turning a read into a side
1392    /// effect. This reads the roster entry's own recorded fields and nothing
1393    /// else.
1394    ///
1395    /// # Errors
1396    /// Returns [`ChannelRegistryError::RosterUnavailable`] only.
1397    pub fn channel_status(&self, name: &str) -> Result<ChannelStatus, ChannelRegistryError> {
1398        let configured = {
1399            let channels = self.read_roster()?;
1400            channels.get(name).map(Arc::clone)
1401        };
1402        Ok(
1403            configured.map_or(ChannelStatus::NotRegistered, |configured| {
1404                status_of(&configured)
1405            }),
1406        )
1407    }
1408
1409    /// Every roster entry with its status, sorted by name, from ONE roster
1410    /// read (SRV-012 R7: the console's topology snapshot).
1411    ///
1412    /// # Errors
1413    /// Returns [`ChannelRegistryError::RosterUnavailable`] when the roster
1414    /// lock is poisoned.
1415    pub fn channel_census(&self) -> Result<Vec<(String, ChannelStatus)>, ChannelRegistryError> {
1416        let mut census: Vec<(String, ChannelStatus)> = self
1417            .read_roster()?
1418            .iter()
1419            .map(|(name, configured)| (name.clone(), status_of(configured)))
1420            .collect();
1421        census.sort_by(|left, right| left.0.cmp(&right.0));
1422        Ok(census)
1423    }
1424
1425    /// The whole roster: one minimal descriptor per entry, sorted by name.
1426    ///
1427    /// The census companion to [`Self::channel_status`]. A by-name probe answers
1428    /// about a name the caller already suspects; only an enumeration can reveal
1429    /// a name the caller does not know to ask about, and a verification sweep
1430    /// with no population denominator is an instrument shape this estate
1431    /// forbids. Touches no actor, under the same constraint as the probe.
1432    ///
1433    /// # Errors
1434    /// Returns [`ChannelRegistryError::RosterUnavailable`] only.
1435    pub fn registered_channels(&self) -> Result<Vec<ChannelDescriptor>, ChannelRegistryError> {
1436        let entries: Vec<(String, Arc<ConfiguredChannel>)> = {
1437            let channels = self.read_roster()?;
1438            channels
1439                .iter()
1440                .map(|(name, configured)| (name.clone(), Arc::clone(configured)))
1441                .collect()
1442        };
1443        let mut descriptors: Vec<ChannelDescriptor> = entries
1444            .into_iter()
1445            .map(|(name, configured)| ChannelDescriptor {
1446                name,
1447                origin: configured.origin,
1448                state: configured.state(),
1449            })
1450            .collect();
1451        descriptors.sort_by(|left, right| left.name.cmp(&right.name));
1452        Ok(descriptors)
1453    }
1454
1455    /// The roster admission funnel: the ONE place a channel operation's
1456    /// permission is decided.
1457    ///
1458    /// Returns the admitted entry, so the caller works from the value the
1459    /// decision was made on rather than reading the roster a second time and
1460    /// risking a different answer. This read is the LINEARISATION POINT for the
1461    /// quiesce race: everything the caller does with the returned entry happens
1462    /// outside the lock and is not re-checked, which is exactly why a quiesce
1463    /// that commits after this returns does not stop the operation it admitted.
1464    ///
1465    /// # Errors
1466    /// Returns [`ChannelAccessError`] when the channel is absent, quiesced, or
1467    /// the roster lock is poisoned.
1468    fn admit_channel(&self, channel: &str) -> Result<Arc<ConfiguredChannel>, ChannelAccessError> {
1469        let configured = {
1470            let channels = self.channels.read().map_err(|_poisoned| {
1471                ChannelAccessError::RosterUnavailable {
1472                    message: ROSTER_POISONED.to_owned(),
1473                }
1474            })?;
1475            channels.get(channel).map(Arc::clone)
1476        };
1477        let configured = configured.ok_or_else(|| ChannelAccessError::NotRegistered {
1478            name: channel.to_owned(),
1479        })?;
1480        if configured.state.load(Ordering::Acquire) == STATE_QUIESCED {
1481            return Err(ChannelAccessError::Quiesced {
1482                name: channel.to_owned(),
1483                reason: configured.recorded_quiesce_reason(),
1484            });
1485        }
1486        Ok(configured)
1487    }
1488
1489    /// Takes the roster's READ guard for the registration surface, mapping a
1490    /// poisoned lock to a typed [`ChannelRegistryError`].
1491    ///
1492    /// Separate from [`Self::read_channels`] because the two surfaces answer
1493    /// different callers with different error types; recovering one from the
1494    /// other would mean inspecting a message.
1495    fn read_roster(
1496        &self,
1497    ) -> Result<
1498        std::sync::RwLockReadGuard<'_, HashMap<String, Arc<ConfiguredChannel>>>,
1499        ChannelRegistryError,
1500    > {
1501        #[cfg(test)]
1502        ROSTER_READS.with(|reads| reads.set(reads.get() + 1));
1503        self.channels
1504            .read()
1505            .map_err(|_poisoned| ChannelRegistryError::RosterUnavailable {
1506                message: ROSTER_POISONED.to_owned(),
1507            })
1508    }
1509
1510    /// Takes the roster's WRITE guard — held only for the check-and-insert that
1511    /// must be atomic, never across a call into the liminal library.
1512    fn write_roster(
1513        &self,
1514    ) -> Result<
1515        std::sync::RwLockWriteGuard<'_, HashMap<String, Arc<ConfiguredChannel>>>,
1516        ChannelRegistryError,
1517    > {
1518        self.channels
1519            .write()
1520            .map_err(|_poisoned| ChannelRegistryError::RosterUnavailable {
1521                message: ROSTER_POISONED.to_owned(),
1522            })
1523    }
1524
1525    /// Gives back the tap slot a subscribe to `channel` reserved at
1526    /// admission, taking its tap channel off the roster when no tap stands
1527    /// behind it (SRV-012 R6).
1528    fn give_back_tap(&self, channel: &str) {
1529        self.console.release_tap(channel, |subject| {
1530            crate::console::ServerPublisher::retire_console_channel(self, subject)
1531        });
1532    }
1533
1534    /// The trait's subscribe, from the console guard to the subscription;
1535    /// [`ConnectionServices::subscribe`] gives back the tap reservation when
1536    /// this fails.
1537    fn open_subscription(
1538        &self,
1539        channel: &str,
1540        accepted_schemas: &[ProtocolSchemaId],
1541        install: Option<liminal::channel::InboxInstall>,
1542        consumer: Option<&str>,
1543    ) -> Result<ConnectionSubscription, ServerError> {
1544        // The frame path refused this already at admission; this guard is for
1545        // a caller holding the trait directly.
1546        refuse_console_tap_target(channel).map_err(|error| access_to_server_error(&error))?;
1547        // As in `publish`: the admission funnel clones the entry's `Arc` out
1548        // under the read guard and the guard is released before schema
1549        // negotiation or the actor round-trip below. That release is what makes
1550        // this the linearisation point — a quiesce landing after it does not
1551        // stop the subscription this call is already building.
1552        let configured = self
1553            .admit_channel(channel)
1554            .map_err(|error| access_to_server_error(&error))?;
1555        let selected_schema = if accepted_schemas.is_empty() {
1556            configured.protocol_schema
1557        } else {
1558            liminal::protocol::negotiate_schema(configured.protocol_schema, accepted_schemas)
1559                .map_err(|error| server_error_from_protocol(&error))?
1560        };
1561        self.build_subscription(
1562            channel,
1563            &configured.handle,
1564            selected_schema,
1565            install,
1566            consumer,
1567        )
1568    }
1569
1570    fn build_subscription(
1571        &self,
1572        channel: &str,
1573        handle: &ChannelHandle,
1574        selected_schema: ProtocolSchemaId,
1575        install: Option<liminal::channel::InboxInstall>,
1576        consumer: Option<&str>,
1577    ) -> Result<ConnectionSubscription, ServerError> {
1578        // `subscribe_with_install` installs the §5 budget/fairness cap and the R3
1579        // wake notifier on the inbox at construction — strictly before the
1580        // registration is published to the channel actor — so there is no window
1581        // in which a publish can land uncharged or without a wake.
1582        let subscription = match (consumer, install) {
1583            (Some(consumer), Some(install)) => handle.subscribe_consumer(consumer, install),
1584            (Some(_), None) => {
1585                return Err(ServerError::ListenerAccept {
1586                    message: "a durable consumer requires a bounded inbox install".to_owned(),
1587                });
1588            }
1589            (None, Some(install)) => handle.subscribe_with_install(install),
1590            (None, None) => handle.subscribe(),
1591        }
1592        .map_err(|error| ServerError::ListenerAccept {
1593            message: format!("liminal subscribe failed for channel '{channel}': {error}"),
1594        })?;
1595        let id = self.next_subscription_id.fetch_add(1, Ordering::Relaxed);
1596        let mut connection_subscription = ConnectionSubscription::new(
1597            id,
1598            selected_schema,
1599            Box::new(LiminalSubscriptionResource { subscription }),
1600        );
1601        connection_subscription.set_channel(channel.to_owned());
1602        // SRV-012 R6: the console tap attaches and detaches through the
1603        // supervisor's observer, which hears every subscribe and every
1604        // departure once (ServerObserver).
1605        Ok(connection_subscription)
1606    }
1607
1608    pub(crate) fn subscribe_consumer(
1609        &self,
1610        channel: &str,
1611        consumer: &str,
1612        install: liminal::channel::InboxInstall,
1613    ) -> Result<ConnectionSubscription, ServerError> {
1614        self.open_subscription(channel, &[], Some(install), Some(consumer))
1615    }
1616
1617    pub(crate) fn install_exporter(
1618        &self,
1619        exporter: &Arc<crate::link_bridge::subscriptions::ExportSubscriptions>,
1620    ) -> Result<(), ServerError> {
1621        let mut exporters =
1622            self.export_subscriptions
1623                .lock()
1624                .map_err(|_| ServerError::ListenerAccept {
1625                    message: "export subscription registry lock poisoned".to_owned(),
1626                })?;
1627        exporters.retain(|exporter| exporter.strong_count() > 0);
1628        if exporters
1629            .iter()
1630            .filter_map(std::sync::Weak::upgrade)
1631            .any(|existing| existing.link() == exporter.link())
1632        {
1633            return Err(ServerError::ListenerAccept {
1634                message: format!(
1635                    "link '{}' already owns an export subscription",
1636                    exporter.link()
1637                ),
1638            });
1639        }
1640        let mut prepared = Vec::new();
1641        for channel in self
1642            .registered_channels()
1643            .map_err(|error| ServerError::ListenerAccept {
1644                message: error.to_string(),
1645            })?
1646        {
1647            if matches!(channel.state, ChannelState::Active) {
1648                if let Some(binding) = exporter.binding(&channel.name) {
1649                    let subscription =
1650                        self.subscribe_consumer(&channel.name, &binding.consumer, binding.install)?;
1651                    prepared.push(
1652                        crate::link_bridge::subscriptions::PreparedSubscription::new(
1653                            binding.prefix,
1654                            channel.name,
1655                            subscription,
1656                        )?,
1657                    );
1658                }
1659            }
1660        }
1661        for subscription in prepared {
1662            exporter.attach(subscription);
1663        }
1664        exporters.push(Arc::downgrade(exporter));
1665        drop(exporters);
1666        Ok(())
1667    }
1668
1669    /// Refuses a subscribe that would start one more live tap than
1670    /// [`crate::console::MAX_LIVE_TAPS`] (SRV-012 R6); a subscriber to a tap
1671    /// already live is not a new tap.
1672    ///
1673    /// A poisoned tap table does not refuse the subscribe (R8: a console
1674    /// bookkeeping failure never fails the client operation it watches); the
1675    /// attach that follows meets the poison and counts it.
1676    ///
1677    /// # Errors
1678    /// Returns [`ChannelAccessError::ConsoleTapLimit`] naming the channel.
1679    fn refuse_console_tap_limit(&self, channel: &str) -> Result<(), ChannelAccessError> {
1680        match self.console.reserve_tap(channel) {
1681            Ok(false) => Err(ChannelAccessError::ConsoleTapLimit {
1682                name: channel.to_owned(),
1683                limit: crate::console::MAX_LIVE_TAPS,
1684            }),
1685            Ok(true) | Err(_) => Ok(()),
1686        }
1687    }
1688}
1689
1690impl crate::console::ServerPublisher for LiminalConnectionServices {
1691    /// The server's own record on the one publish path, with a server origin:
1692    /// attributed to `liminal.<device>`, and never counted as a connection's
1693    /// work by the cost tick.
1694    fn publish_console(
1695        &self,
1696        subject: &str,
1697        payload: &[u8],
1698    ) -> Result<(), crate::console::ConsoleError> {
1699        let envelope = MessageEnvelope::new(
1700            ProtocolSchemaId::new([0_u8; ProtocolSchemaId::WIRE_LEN]),
1701            liminal::protocol::CausalContext::independent(),
1702            payload.to_vec(),
1703        );
1704        self.publish_with_origin(
1705            subject,
1706            &envelope,
1707            None,
1708            PublishOrigin::Server {
1709                device: self.console.device(),
1710            },
1711        )
1712        .map(|_published| ())
1713        .map_err(|source| crate::console::ConsoleError::Server {
1714            subject: subject.to_owned(),
1715            source,
1716        })
1717    }
1718
1719    /// Takes a `console.tap.<target>` channel off the roster (SRV-012 R6).
1720    /// Only a console subject is ever removed here.
1721    fn retire_console_channel(&self, subject: &str) -> Result<(), crate::console::ConsoleError> {
1722        if !super::channel_registry::is_console_subject(subject) {
1723            return Ok(());
1724        }
1725        let mut channels =
1726            self.write_roster()
1727                .map_err(|error| crate::console::ConsoleError::Server {
1728                    subject: subject.to_owned(),
1729                    source: ServerError::ListenerAccept {
1730                        message: error.to_string(),
1731                    },
1732                })?;
1733        channels.remove(subject);
1734        drop(channels);
1735        Ok(())
1736    }
1737}
1738
1739/// The production participant service and the operator surfaces taken from
1740/// its one live handler before it is erased behind
1741/// `dyn ParticipantSemanticHandler`.
1742struct InstalledParticipant {
1743    service: InstalledParticipantService,
1744    unloadable_conversations: UnloadableConversationRecord,
1745    credential_reissuer: Arc<dyn OperatorCredentialReissuer>,
1746    warm: ParticipantWarm,
1747}
1748
1749/// Production participant activation (LP gap closure, Part B): the
1750/// deployment's [participant] section installs the ONE production semantic
1751/// handler, sealed together with the same durable store the conversation logs
1752/// live in, under the configured wire-frame limit.
1753///
1754/// # Errors
1755/// Returns [`ServerError::ParticipantStartupRestore`] when the handler cannot
1756/// be built or wired, and [`ServerError::ConfigValidation`] when the
1757/// wire-frame limit is below the protocol's minimum participant frame.
1758fn install_participant(
1759    durable_store: &Arc<dyn DurableStore>,
1760    participant: &ParticipantConfig,
1761    signals: &ServerSignals,
1762) -> Result<InstalledParticipant, ServerError> {
1763    let handler = ProductionParticipantHandler::new(Arc::clone(durable_store), *participant)
1764        .map_err(|error| ServerError::ParticipantStartupRestore {
1765            message: error.to_string(),
1766        })?;
1767    // Taken BEFORE the handler is erased behind
1768    // `dyn ParticipantSemanticHandler`. Construction restores no
1769    // conversation (DUR-020), so the record fills as conversations restore,
1770    // on first touch or in the warm, and is complete once the warm has
1771    // finished.
1772    handler.install_signals(signals.clone()).map_err(|error| {
1773        ServerError::ParticipantStartupRestore {
1774            message: error.to_string(),
1775        }
1776    })?;
1777    let unloadable_conversations = handler.unloadable_record();
1778    let handler = Arc::new(handler);
1779    let warm = handler
1780        .install_warm()
1781        .map_err(|error| ServerError::ParticipantStartupRestore {
1782            message: error.to_string(),
1783        })?;
1784    // A7 (§0.18): taken here for the same reason, and from the same one live
1785    // handler, so the operator surface and the participant wire cannot end up
1786    // talking to two different owners.
1787    let credential_reissuer: Arc<dyn OperatorCredentialReissuer> =
1788        Arc::<ProductionParticipantHandler>::clone(&handler);
1789    let service = InstalledParticipantService::new(
1790        handler,
1791        Arc::clone(durable_store),
1792        participant.wire_frame_limit,
1793    )
1794    .map_err(|error| ServerError::ConfigValidation {
1795        message: format!(
1796            "participant.wire_frame_limit: {} is below the protocol's minimum \
1797             complete participant frame ({error:?})",
1798            participant.wire_frame_limit
1799        ),
1800    })?;
1801    Ok(InstalledParticipant {
1802        service,
1803        unloadable_conversations,
1804        credential_reissuer,
1805        warm,
1806    })
1807}
1808
1809/// The console hub for `device`, its model built from the roster as it
1810/// stands (responders, connections and peers arrive as changes).
1811///
1812/// # Errors
1813/// Returns [`ServerError::ListenerAccept`] when the device is blank or the
1814/// wall clock cannot be read.
1815fn console_hub(
1816    device: &str,
1817    channels: &HashMap<String, Arc<ConfiguredChannel>>,
1818    depot: bool,
1819    lys: Option<&crate::config::types::LysConfig>,
1820) -> Result<crate::console::ConsoleHub, ServerError> {
1821    let this = crate::console::ThisDevice {
1822        id: device.to_owned(),
1823        liminal: env!("CARGO_PKG_VERSION").to_owned(),
1824        started: Instant::now(),
1825        depot,
1826    };
1827    let lines = channels.iter().map(|(name, configured)| {
1828        let state = match configured.state() {
1829            ChannelState::Active => crate::console::records::LineState::Live,
1830            ChannelState::Quiesced { .. } => crate::console::records::LineState::Quiesced,
1831        };
1832        (name.clone(), line_life(configured.durable), state)
1833    });
1834    let mut model = crate::console::TopologyModel::new(this, [], lines, [], []);
1835    if let Some(machine) = lys.and_then(|lys| lys.machine.as_ref()) {
1836        model = model.with_machine(machine);
1837    }
1838    crate::console::wall::WallBase::now()
1839        .and_then(|wall| crate::console::ConsoleHub::new(device, model, wall))
1840        .map_err(|error| ServerError::ListenerAccept {
1841            message: format!("console hub: {error}"),
1842        })
1843}
1844
1845/// A roster entry's status as [`LiminalConnectionServices::channel_status`]
1846/// answers it.
1847fn status_of(configured: &ConfiguredChannel) -> ChannelStatus {
1848    let mode = configured.handle.config().mode;
1849    match configured.state() {
1850        ChannelState::Active => ChannelStatus::Active {
1851            origin: configured.origin,
1852            mode,
1853            schema: configured.protocol_schema,
1854        },
1855        ChannelState::Quiesced { reason } => ChannelStatus::Quiesced {
1856            reason,
1857            origin: configured.origin,
1858            mode,
1859        },
1860    }
1861}
1862
1863#[cfg(test)]
1864thread_local! {
1865    /// Roster read locks taken on this thread. Test-only.
1866    static ROSTER_READS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
1867}
1868
1869/// Roster read locks taken on the calling thread so far. Test-only.
1870#[cfg(test)]
1871pub(crate) fn roster_reads() -> u64 {
1872    ROSTER_READS.with(std::cell::Cell::get)
1873}
1874
1875/// A channel's line life: durable channels keep, ephemeral ones pass.
1876const fn line_life(durable: bool) -> crate::console::records::LineLife {
1877    if durable {
1878        crate::console::records::LineLife::Kept
1879    } else {
1880        crate::console::records::LineLife::Passing
1881    }
1882}
1883
1884/// The diagnostic carried when the roster lock is poisoned. One string, shared
1885/// by every surface that reports it, so the wording cannot drift between them.
1886const ROSTER_POISONED: &str = "channel roster lock poisoned";
1887
1888/// How many roster entries the registration cap counts.
1889///
1890/// Runtime-registered entries ONLY. Boot-configured channels are the operator's
1891/// own authored bound — they are in the file the operator wrote — and counting
1892/// them would make one number mean two different things depending on how the
1893/// deployment was configured. The origin never flips, so this population is
1894/// well defined over time.
1895fn runtime_registered_count(channels: &HashMap<String, Arc<ConfiguredChannel>>) -> usize {
1896    channels
1897        .values()
1898        .filter(|configured| configured.origin == ChannelOrigin::RuntimeRegistered)
1899        .count()
1900}
1901
1902/// The three-field identity comparison behind idempotent-if-identical
1903/// registration.
1904///
1905/// All three must match; the FIRST mismatch is reported by name. The name itself
1906/// is not compared — it is the roster key, a precondition of the comparison
1907/// rather than a member of it — and neither are the supervisor or the durable
1908/// store, which are one server-wide instance each and cannot differ between two
1909/// registrations in one process.
1910///
1911/// The schema is compared as BOTH its protocol id and its parsed document, on
1912/// purpose. The id is a 64-bit non-cryptographic digest, so the document guards
1913/// against a collision accepting a different schema as identical; and the two
1914/// fail in opposite directions, because two byte sequences that parse to the
1915/// same document but differ in whitespace produce different ids — which must
1916/// refuse, since the id is what every future subscriber negotiates.
1917///
1918/// The channel's own `Schema` is not compared: it is not `PartialEq`, and its
1919/// identifier is a fresh value per construction, so comparing it would refuse
1920/// every idempotent re-registration.
1921fn compare_registration(
1922    existing: &ConfiguredChannel,
1923    spec: &ChannelRegistration,
1924    resolved: &ChannelSchema,
1925) -> Result<Registered, ChannelRegistryError> {
1926    let requested_mode = if spec.durable {
1927        ChannelMode::Durable
1928    } else {
1929        ChannelMode::Ephemeral
1930    };
1931    let config = existing.handle.config();
1932    let mismatch = if config.mode == requested_mode {
1933        if existing.protocol_schema == resolved.protocol_id {
1934            if *config.schema.definition() == resolved.document {
1935                None
1936            } else {
1937                Some(ChannelConfigField::SchemaDocument)
1938            }
1939        } else {
1940            Some(ChannelConfigField::SchemaId)
1941        }
1942    } else {
1943        Some(ChannelConfigField::Mode)
1944    };
1945    mismatch.map_or(Ok(Registered::AlreadyIdentical), |field| {
1946        Err(ChannelRegistryError::AlreadyRegistered {
1947            name: spec.name.clone(),
1948            field,
1949        })
1950    })
1951}
1952
1953/// Recovers the resolved schema of an already-built entry, so the write-lock
1954/// re-check compares the SAME three fields against the same values the fast path
1955/// would have.
1956fn schema_of(configured: &ConfiguredChannel) -> ChannelSchema {
1957    ChannelSchema {
1958        document: configured.handle.config().schema.definition().clone(),
1959        protocol_id: configured.protocol_schema,
1960    }
1961}
1962
1963/// Builds one roster entry: the channel's validation engine, its library handle
1964/// (durable or ephemeral), and the protocol schema id advertised at subscribe
1965/// time.
1966///
1967/// This is the SOLE place a [`ConfiguredChannel`] is constructed. Keeping
1968/// construction in one function is what makes "every channel on this server is
1969/// the same kind of object" structural rather than a promise: there is no second
1970/// body a channel could be built by, so no channel can drift into a different
1971/// shape. The boot loop in [`LiminalConnectionServices::from_config_with_store_via`]
1972/// is its caller.
1973///
1974/// `resolved` is consumed because its JSON Schema document is moved into the
1975/// channel's [`Schema`]; the protocol id is carried onto the entry unchanged.
1976/// `origin` is stamped here and never written again.
1977///
1978/// The failure is typed ([`ChannelBuildError`]) rather than a [`ServerError`] so
1979/// BOTH callers can render it without inspecting a message: the boot loop maps
1980/// it back to the exact `ConfigValidation` strings it has always produced, and
1981/// `register_channel` maps it to the registry's own typed variants.
1982///
1983/// # Errors
1984/// Returns [`ChannelBuildError`] when the JSON Schema document does not compile
1985/// or durable initialization over the shared store fails.
1986fn build_configured_channel(
1987    name: &str,
1988    resolved: ChannelSchema,
1989    durable: bool,
1990    origin: ChannelOrigin,
1991    durable_store: &Arc<dyn DurableStore>,
1992    supervisor: &ChannelSupervisor,
1993) -> Result<ConfiguredChannel, ChannelBuildError> {
1994    let schema =
1995        Schema::new(resolved.document).map_err(|error| ChannelBuildError::SchemaRejected {
1996            message: error.to_string(),
1997        })?;
1998    let channel_config = if durable {
1999        ChannelConfig::new(name.to_owned(), schema, ChannelMode::Durable)
2000    } else {
2001        ChannelConfig::new(name.to_owned(), schema, ChannelMode::Ephemeral)
2002    };
2003    let handle = if durable {
2004        ChannelHandle::new_durable_with_supervisor(
2005            channel_config,
2006            Arc::clone(durable_store),
2007            supervisor.clone(),
2008        )
2009        .map_err(|error| ChannelBuildError::DurableInitFailed {
2010            message: error.to_string(),
2011        })?
2012    } else {
2013        ChannelHandle::with_supervisor(channel_config, supervisor.clone())
2014    };
2015    Ok(ConfiguredChannel {
2016        handle,
2017        protocol_schema: resolved.protocol_id,
2018        durable,
2019        origin,
2020        state: AtomicU8::new(STATE_ACTIVE),
2021        quiesce_reason: OnceLock::new(),
2022    })
2023}
2024
2025/// Returns the current epoch-millis timestamp used as the dedup entry anchor.
2026///
2027/// A clock error before the Unix epoch yields `0`: the timestamp is only a TTL
2028/// anchor for the in-memory cache and a zero anchor never breaks the at-most-once
2029/// claim semantics, so this avoids surfacing a clock fault on the publish path.
2030fn dedup_timestamp_millis() -> u64 {
2031    use std::time::{SystemTime, UNIX_EPOCH};
2032    SystemTime::now()
2033        .duration_since(UNIX_EPOCH)
2034        .ok()
2035        .and_then(|duration| u64::try_from(duration.as_millis()).ok())
2036        .unwrap_or(0)
2037}
2038
2039/// Default shard count for an on-disk durable store.
2040///
2041/// Haematite routes keys across this many single-threaded shard actors; a small
2042/// power of two gives parallelism across cursors/streams without spawning an
2043/// actor per core. The value is fixed (haematite has no silent default) and not
2044/// yet surfaced in server config.
2045const DEFAULT_SHARD_COUNT: usize = 8;
2046
2047/// Shard count of the throwaway store an incarnation's ephemeral dedup
2048/// namespace lives in: it holds one namespace, and dies with the process.
2049const THROWAWAY_SHARD_COUNT: usize = 1;
2050
2051/// Namespace prefix for the dedup-on-delivery cache streams. Keeps delivery dedup
2052/// keys from colliding with any other haematite streams in the shared store.
2053///
2054/// This is the **durable** namespace (A1 §5): stable across restarts, because a
2055/// durable channel's message outlives a restart too.
2056const DELIVERY_DEDUP_NAMESPACE: &str = "liminal:delivery-dedup";
2057
2058/// The **ephemeral** dedup namespace for this server incarnation (A1 §5,
2059/// graft §0.2).
2060///
2061/// Scoped to the process incarnation so an ephemeral channel's receipt shares a
2062/// lifetime with the in-memory message it vouches for. Composed from the
2063/// process id and the boot instant, which is enough to make two incarnations
2064/// disagree; nothing reads the components back, so no meaning is attached to
2065/// them beyond distinctness.
2066///
2067/// The namespace lives in [`ephemeral_dedup_store`], never in a store that
2068/// outlives the process, so no incarnation's records are left behind for a
2069/// later one to read past.
2070fn ephemeral_dedup_namespace() -> String {
2071    use std::time::{SystemTime, UNIX_EPOCH};
2072    let boot_nanos = SystemTime::now()
2073        .duration_since(UNIX_EPOCH)
2074        .map_or(0, |since| since.as_nanos());
2075    format!(
2076        "{DELIVERY_DEDUP_NAMESPACE}:incarnation:{}-{boot_nanos}",
2077        std::process::id()
2078    )
2079}
2080
2081/// One reader over the counters of `durable_store` and, when it is a separate
2082/// store, `ephemeral_store`; a store that is the same `Arc` is read once.
2083fn combined_store_counts(
2084    durable_store: &Arc<dyn DurableStore>,
2085    ephemeral_store: &Arc<dyn DurableStore>,
2086) -> StoreCountsHandle {
2087    let durable = durable_store.store_counts().unwrap_or_default();
2088    if Arc::ptr_eq(durable_store, ephemeral_store) {
2089        return durable;
2090    }
2091    durable.with(&ephemeral_store.store_counts().unwrap_or_default())
2092}
2093
2094/// The store an incarnation's ephemeral dedup namespace is written to: the
2095/// services' own store when it was created empty for this process and dies
2096/// with it, otherwise a store of its own that does, opened by the namespace's
2097/// first write rather than at start.
2098///
2099/// Written into a persistent store, every incarnation's namespace would stay
2100/// there for good, under the durable namespace's prefix, with nothing that
2101/// ever reads it again to prune it.
2102fn ephemeral_dedup_store(
2103    durable_store: &Arc<dyn DurableStore>,
2104    history: StoreHistory,
2105    subsystems: &dyn SubsystemFactory,
2106) -> Arc<dyn DurableStore> {
2107    match (history, durable_store) {
2108        #[cfg(any(test, feature = "test-support"))]
2109        (StoreHistory::Fresh, store) => Arc::clone(store),
2110        (StoreHistory::Created | StoreHistory::Recorded, _) => Arc::new(CountingStore::new(
2111            DeferredStore::new(subsystems.throwaway_store()),
2112        )),
2113    }
2114}
2115
2116/// Beamr-scheduler-owning subsystems the full-service construction path builds
2117/// beyond the connection supervisor's own scheduler. The §9 D2 seam census counts
2118/// these: the worker-front-door profile must construct NONE of them. Test-gated:
2119/// this is the recording vocabulary of the gate's instrument, not production state.
2120#[cfg(test)]
2121#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2122pub(super) enum SchedulerSubsystem {
2123    /// The shared channel supervisor (its own beamr scheduler).
2124    ChannelSupervisor,
2125    /// The conversation supervisor (its own beamr scheduler).
2126    ConversationSupervisor,
2127    /// The haematite store's database (its shard-actor scheduler).
2128    HaematiteStore,
2129}
2130
2131/// Constructor seam for every scheduler-owning subsystem (`SchedulerSubsystem`)
2132/// the profile-aware construction path can create.
2133///
2134/// These methods are the ONLY route through which [`build_connection_services`]
2135/// and the [`LiminalConnectionServices`] config constructors reach
2136/// `build_channel_cluster`, `ConversationSupervisor::new`, and the durable-store
2137/// constructors — no direct constructor call exists in those bodies. The §9 D2
2138/// gate therefore injects a factory that records as a side effect of
2139/// constructing (the D3 store-seam ownership move applied to schedulers): a
2140/// recording cannot be omitted without also failing to construct the subsystem,
2141/// which closes the "hand-placed census call beside the constructor" gap where a
2142/// future subsystem could be built without its courtesy call.
2143pub(super) trait SubsystemFactory {
2144    /// Constructs the shared channel supervisor + cluster resolver (a beamr
2145    /// scheduler).
2146    ///
2147    /// # Errors
2148    /// Returns [`ServerError`] when the channel supervisor scheduler cannot start.
2149    fn channel_cluster(
2150        &self,
2151        cluster_config: Option<&ClusterConfig>,
2152    ) -> Result<ChannelCluster, ServerError>;
2153
2154    /// Constructs the conversation supervisor (a beamr scheduler).
2155    ///
2156    /// # Errors
2157    /// Returns [`ServerError`] when the conversation supervisor scheduler cannot
2158    /// start.
2159    fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError>;
2160
2161    /// Constructs the persistent durable store under the explicit directory.
2162    ///
2163    /// # Errors
2164    /// Returns [`ServerError`] when the store cannot be opened.
2165    fn durable_store(&self, persistence_path: &Path) -> Result<Arc<dyn DurableStore>, ServerError>;
2166
2167    /// The opener of the throwaway store an incarnation's ephemeral dedup
2168    /// namespace lives in when the durable store outlives the process. The
2169    /// store is opened by the namespace's first write, never by this call.
2170    fn throwaway_store(&self) -> ThrowawayStoreOpener;
2171
2172    fn ephemeral_sweep_counts(&self) -> Option<EphemeralSweepCounts> {
2173        None
2174    }
2175}
2176
2177/// Test construction factory for callers that own their fixture directories.
2178#[cfg(any(test, feature = "test-support"))]
2179pub(super) struct ProductionSubsystems;
2180
2181#[cfg(any(test, feature = "test-support"))]
2182impl SubsystemFactory for ProductionSubsystems {
2183    fn channel_cluster(
2184        &self,
2185        cluster_config: Option<&ClusterConfig>,
2186    ) -> Result<ChannelCluster, ServerError> {
2187        build_channel_cluster(cluster_config)
2188    }
2189
2190    fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError> {
2191        Ok(Arc::new(ConversationSupervisor::new().map_err(
2192            |error| ServerError::ConfigValidation {
2193                message: format!("failed to start conversation supervisor: {error}"),
2194            },
2195        )?))
2196    }
2197
2198    fn durable_store(&self, persistence_path: &Path) -> Result<Arc<dyn DurableStore>, ServerError> {
2199        build_durable_store(persistence_path)
2200    }
2201
2202    fn throwaway_store(&self) -> ThrowawayStoreOpener {
2203        Box::new(|| open_ephemeral(THROWAWAY_SHARD_COUNT))
2204    }
2205}
2206
2207struct RootedProductionSubsystems {
2208    root: Arc<EphemeralRoot>,
2209}
2210
2211impl RootedProductionSubsystems {
2212    fn new(path: &Path) -> Result<Self, ServerError> {
2213        let root = EphemeralRoot::open(path).map_err(|error| ServerError::ConfigValidation {
2214            message: error.to_string(),
2215        })?;
2216        Ok(Self { root })
2217    }
2218}
2219
2220impl SubsystemFactory for RootedProductionSubsystems {
2221    fn channel_cluster(
2222        &self,
2223        config: Option<&ClusterConfig>,
2224    ) -> Result<ChannelCluster, ServerError> {
2225        build_channel_cluster(config)
2226    }
2227
2228    fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError> {
2229        Ok(Arc::new(ConversationSupervisor::new().map_err(
2230            |error| ServerError::ConfigValidation {
2231                message: format!("failed to start conversation supervisor: {error}"),
2232            },
2233        )?))
2234    }
2235
2236    fn durable_store(&self, path: &Path) -> Result<Arc<dyn DurableStore>, ServerError> {
2237        build_durable_store(path)
2238    }
2239
2240    fn throwaway_store(&self) -> ThrowawayStoreOpener {
2241        let root = Arc::clone(&self.root);
2242        Box::new(move || root.open_store(THROWAWAY_SHARD_COUNT))
2243    }
2244
2245    fn ephemeral_sweep_counts(&self) -> Option<EphemeralSweepCounts> {
2246        Some(self.root.sweep_counts())
2247    }
2248}
2249
2250/// Test-only recording implementation of [`SubsystemFactory`] for the §9 D2
2251/// construction gate. Lives here (not in a test module's private scope) so the
2252/// supervisor-level gate test reuses the same instrument.
2253#[cfg(test)]
2254#[allow(clippy::expect_used)]
2255pub(super) mod subsystem_census {
2256    use std::path::{Path, PathBuf};
2257    use std::sync::{Arc, Mutex};
2258
2259    use liminal::conversation::ConversationSupervisor;
2260    use liminal::durability::{DurableStore, open_ephemeral_rooted};
2261
2262    use super::{
2263        ChannelCluster, ProductionSubsystems, SchedulerSubsystem, SubsystemFactory,
2264        THROWAWAY_SHARD_COUNT, ThrowawayStoreOpener, build_durable_store,
2265    };
2266    use crate::ServerError;
2267    use crate::config::types::ClusterConfig;
2268
2269    /// Records each [`SchedulerSubsystem`] AS A SIDE EFFECT of constructing it,
2270    /// then hands back the production-constructed subsystem (with ephemeral
2271    /// stores rooted in an isolated directory, so the fs half of the gate is a
2272    /// real negative assertion). Because [`SubsystemFactory`] is the only route
2273    /// the profile-aware construction path has to these constructors, a
2274    /// recording cannot be omitted without also failing to construct — the
2275    /// record-by-construction guarantee.
2276    pub struct RecordingSubsystems {
2277        census: Mutex<Vec<SchedulerSubsystem>>,
2278        ephemeral_root: PathBuf,
2279    }
2280
2281    impl RecordingSubsystems {
2282        /// A recording factory whose ephemeral stores live under `ephemeral_root`.
2283        pub fn rooted(ephemeral_root: &Path) -> Self {
2284            Self {
2285                census: Mutex::new(Vec::new()),
2286                ephemeral_root: ephemeral_root.to_path_buf(),
2287            }
2288        }
2289
2290        /// The recorded construction census, sorted for order-independent
2291        /// comparison.
2292        pub fn recorded(&self) -> Vec<SchedulerSubsystem> {
2293            let mut recorded = self
2294                .census
2295                .lock()
2296                .expect("subsystem census lock is never poisoned in tests")
2297                .clone();
2298            recorded.sort();
2299            recorded
2300        }
2301
2302        fn record(&self, subsystem: SchedulerSubsystem) {
2303            self.census
2304                .lock()
2305                .expect("subsystem census lock is never poisoned in tests")
2306                .push(subsystem);
2307        }
2308    }
2309
2310    impl SubsystemFactory for RecordingSubsystems {
2311        fn channel_cluster(
2312            &self,
2313            cluster_config: Option<&ClusterConfig>,
2314        ) -> Result<ChannelCluster, ServerError> {
2315            let cluster = ProductionSubsystems.channel_cluster(cluster_config)?;
2316            self.record(SchedulerSubsystem::ChannelSupervisor);
2317            Ok(cluster)
2318        }
2319
2320        fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError> {
2321            let supervisor = ProductionSubsystems.conversation_supervisor()?;
2322            self.record(SchedulerSubsystem::ConversationSupervisor);
2323            Ok(supervisor)
2324        }
2325
2326        fn durable_store(
2327            &self,
2328            persistence_path: &Path,
2329        ) -> Result<Arc<dyn DurableStore>, ServerError> {
2330            let store = build_durable_store(persistence_path)?;
2331            self.record(SchedulerSubsystem::HaematiteStore);
2332            Ok(store)
2333        }
2334
2335        fn throwaway_store(&self) -> ThrowawayStoreOpener {
2336            let root = self.ephemeral_root.clone();
2337            Box::new(move || open_ephemeral_rooted(&root, THROWAWAY_SHARD_COUNT))
2338        }
2339    }
2340}
2341
2342/// Full-only constructor guard: rejects a config whose profile is not `Full`.
2343///
2344/// [`LiminalConnectionServices`]' config-based constructors call this at entry so
2345/// the full service stack can never be built for a worker-front-door config —
2346/// profile enforcement holds on every public construction path, not only the
2347/// file-loading pipeline.
2348fn require_full_profile(config: &ServerConfig) -> Result<(), ServerError> {
2349    match config.services.profile()? {
2350        ServiceProfile::Full => Ok(()),
2351        ServiceProfile::WorkerFrontDoor => Err(ServerError::ConfigValidation {
2352            message: format!(
2353                "services.profile: \"{}\" cannot construct the full LiminalConnectionServices; \
2354                 build profile-selected services via build_connection_services",
2355                ServiceProfile::WORKER_FRONT_DOOR
2356            ),
2357        }),
2358    }
2359}
2360
2361/// Builds the connection-services adapter selected by `config`'s service profile.
2362///
2363/// `Full` builds [`LiminalConnectionServices`] (channels, conversations, durable
2364/// store, dedup cache) exactly as today. `WorkerFrontDoor` builds
2365/// [`WorkerFrontDoorServices`], which constructs none of that machinery. This is the
2366/// single profile-dispatch authority: [`super::supervisor::ConnectionSupervisor`]'s
2367/// config constructor and the standalone runtime's worker arm both route through it
2368/// (the runtime's full arm stays on the explicit
2369/// [`LiminalConnectionServices::from_config`] path because it also needs the shared
2370/// channel cluster, which the trait object does not expose — that path is guarded
2371/// full-only at entry).
2372///
2373/// # Errors
2374/// Returns [`ServerError`] when the selected adapter cannot be constructed, the
2375/// configured profile value is not recognised, or the worker-front-door profile is
2376/// combined with full-only config fields.
2377pub fn build_connection_services(
2378    config: &ServerConfig,
2379) -> Result<Arc<dyn ConnectionServices>, ServerError> {
2380    match config.services.profile()? {
2381        ServiceProfile::Full => {
2382            let subsystems = RootedProductionSubsystems::new(required_persistence_path(config)?)?;
2383            build_connection_services_via(config, &subsystems)
2384        }
2385        ServiceProfile::WorkerFrontDoor => build_worker_front_door(config),
2386    }
2387}
2388
2389/// [`build_connection_services`] with the subsystem factory injected — the §9 D2
2390/// gate seam, both halves at once.
2391///
2392/// Every scheduler-owning subsystem the `Full` branch creates is constructed
2393/// through `subsystems` and nowhere else, so a recording factory observes exactly
2394/// what was built (thread half, record-by-construction); the gate's factory also
2395/// roots its ephemeral stores in an isolated directory, so "the root stays empty
2396/// on the worker branch" is a real negative assertion (fs half, the D3 pattern).
2397/// The `WorkerFrontDoor` branch never touches the factory — a regression that gave
2398/// the front door any subsystem would both record in the census and land a store
2399/// directory in the injected root.
2400///
2401/// The worker branch re-runs the cross-field checks here, not only in file-loading
2402/// validation, so a directly-constructed config cannot smuggle full-only machinery
2403/// past the profile.
2404pub(super) fn build_connection_services_via(
2405    config: &ServerConfig,
2406    subsystems: &dyn SubsystemFactory,
2407) -> Result<Arc<dyn ConnectionServices>, ServerError> {
2408    match config.services.profile()? {
2409        ServiceProfile::Full => {
2410            let path = required_persistence_path(config)?;
2411            let history = StoreHistory::of(path);
2412            let store = subsystems.durable_store(path)?;
2413            Ok(Arc::new(
2414                LiminalConnectionServices::from_config_with_store_via(
2415                    config, store, history, subsystems,
2416                )?,
2417            ))
2418        }
2419        ServiceProfile::WorkerFrontDoor => build_worker_front_door(config),
2420    }
2421}
2422
2423fn build_worker_front_door(
2424    config: &ServerConfig,
2425) -> Result<Arc<dyn ConnectionServices>, ServerError> {
2426    let errors = crate::config::validation::worker_front_door_field_errors(config);
2427    if !errors.is_empty() {
2428        return Err(ServerError::ConfigValidation {
2429            message: errors.join("; "),
2430        });
2431    }
2432    Ok(Arc::new(WorkerFrontDoorServices::new()))
2433}
2434
2435/// Opens the persistent durable store under the configured directory.
2436pub(crate) fn open_persistent_store(
2437    persistence_path: &Path,
2438) -> Result<Arc<dyn DurableStore>, ServerError> {
2439    build_durable_store(persistence_path)
2440}
2441
2442/// Every append is permanent through its WAL frames and one device flush
2443/// before it returns under either commit policy; what the policy decides is
2444/// when the shard folds its staged appends into a new tree version, the fold
2445/// that writes the leaf and every internal node above it as fresh files. Per
2446/// append, one keyed publish left 8 new node files (82,137 bytes) on the J3
2447/// store (NODE-FILES-PER-POST, check 15 on Dean at 68a08b35, 10 October
2448/// 2026), and the live spine reached 2,856,796 files that way. Buffered at
2449/// 1 MiB, Waffles' number (10 October 2026 11:19): the threshold haematite's
2450/// own `LeafLog` runs on, and the most an unclean restart replays from staged
2451/// frames per shard. The store's own reads merge the buffer, so a publish
2452/// reads back at once; a clean stop commits what is staged; no policy carries
2453/// a timer.
2454const SPINE_COMMIT_THRESHOLD_BYTES: NonZeroU64 = match NonZeroU64::new(1 << 20) {
2455    Some(threshold) => threshold,
2456    None => unreachable!(),
2457};
2458
2459/// The commit policy the spine's event store opens under.
2460const SPINE_COMMIT_POLICY: CommitPolicy = CommitPolicy::buffered(SPINE_COMMIT_THRESHOLD_BYTES);
2461
2462fn build_durable_store(path: &Path) -> Result<Arc<dyn DurableStore>, ServerError> {
2463    let data_dir = durable_data_dir(path);
2464    let database = open_or_create_database(&data_dir)?;
2465    let event_store = EventStore::with_commit_policy(database, SPINE_COMMIT_POLICY);
2466    Ok(Arc::new(CountingStore::new(HaematiteStore::new(Arc::new(
2467        event_store,
2468    )))))
2469}
2470
2471/// The directory the persistent database under `persistence_path` lives in.
2472pub(super) fn durable_data_dir(persistence_path: &Path) -> PathBuf {
2473    persistence_path.join("durability")
2474}
2475
2476/// Whether a haematite database already exists at `data_dir`: its
2477/// `config.json` is written when it is created.
2478pub(super) fn database_exists(data_dir: &Path) -> bool {
2479    database_config_file(data_dir).exists()
2480}
2481
2482/// What creating a haematite database leaves in its directory. A shard's
2483/// directory appears beside them the first time anything opens the shard, so
2484/// a database holding nothing else has never been read or written.
2485const UNTOUCHED_DATABASE_ENTRIES: [&str; 2] = ["config.json", "writer.lock"];
2486
2487/// Whether the database at `data_dir` holds only what its creation wrote.
2488/// Anything this cannot read, or does not recognise, answers `false`: the
2489/// database is then taken to hold what an earlier server wrote.
2490pub(super) fn database_untouched(data_dir: &Path) -> bool {
2491    std::fs::read_dir(data_dir).is_ok_and(|entries| {
2492        entries.into_iter().all(|entry| {
2493            entry.is_ok_and(|entry| {
2494                entry
2495                    .file_name()
2496                    .to_str()
2497                    .is_some_and(|name| UNTOUCHED_DATABASE_ENTRIES.contains(&name))
2498            })
2499        })
2500    })
2501}
2502
2503fn database_config_file(data_dir: &Path) -> PathBuf {
2504    data_dir.join("config.json")
2505}
2506
2507/// Every commit writes new copies of the nodes it changes; the durable store
2508/// deletes the copies its committed root no longer reaches while it runs, so it
2509/// holds its live records and not every version ever written. A store created
2510/// under `KeepAll` is moved onto this retention by its next open.
2511pub(super) const DURABLE_RETENTION: Retention = Retention::reclaim(0);
2512
2513/// Opens an existing haematite database at `data_dir`, or creates one.
2514fn open_or_create_database(data_dir: &Path) -> Result<Database, ServerError> {
2515    let config_file = database_config_file(data_dir);
2516    let result = if config_file.exists() {
2517        adopt_missing_config_keys(data_dir, &config_file)?;
2518        Database::open_with_retention(
2519            data_dir,
2520            haematite::RuntimeOptions {
2521                long_wait_interval: std::time::Duration::from_secs(5),
2522                // DUR-021 R1: no pass starts and no step runs until the
2523                // listeners are bound and runtime.rs releases it.
2524                reclaim_start: haematite::ReclaimStart::Held,
2525            },
2526            DURABLE_RETENTION,
2527        )
2528    } else {
2529        Database::create(
2530            DatabaseConfig {
2531                data_dir: data_dir.to_path_buf(),
2532                shard_count: DEFAULT_SHARD_COUNT,
2533                distributed: None,
2534                executor_threads: None,
2535                // haematite 0.8.3 requires this field and refuses `None` at
2536                // validation; `Unlimited` is the crate's explicit spelling of the
2537                // pre-budget (0.8.1) behaviour — the behaviour-preserving choice
2538                // for a dependency hop. A real byte ceiling is a deployment
2539                // decision for its own measured lane. A store whose `config.json`
2540                // predates the field is migrated by the open arm above
2541                // (`adopt_missing_config_keys`) — same value, written once, on
2542                // upgrade.
2543                node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
2544                retention: DURABLE_RETENTION,
2545            },
2546            haematite::RuntimeOptions {
2547                long_wait_interval: std::time::Duration::from_secs(5),
2548                // DUR-021 R1: no pass starts and no step runs until the
2549                // listeners are bound and runtime.rs releases it.
2550                reclaim_start: haematite::ReclaimStart::Held,
2551            },
2552        )
2553    };
2554    let database = result.map_err(|error| ServerError::ConfigValidation {
2555        message: format!(
2556            "failed to open durable store at {}: {error}",
2557            data_dir.display()
2558        ),
2559    })?;
2560    liminal::durability::record_store_opened();
2561    super::reclaim_placement::arm_watch(&database);
2562    Ok(database)
2563}
2564
2565/// The stored keys this crate's create arm always writes, with the value it
2566/// writes, for a `config.json` written before haematite had the key.
2567///
2568/// - `node_cache_budget`: haematite 0.8.3 refuses a store without it
2569///   (`MissingNodeCacheBudget`); `"unlimited"` is the pre-budget behaviour.
2570/// - `retention`: `"keep_all"` is the 0.14.1 behaviour, written in so the
2571///   store's stored retention is explicit before the open. The open itself is
2572///   [`Database::open_with_retention`] under [`DURABLE_RETENTION`], which then
2573///   records reclaim in place of it, so an adopted store reclaims from that
2574///   same open onward (spine reclaim, Hermes's ruling of 6 October 2026:
2575///   `past_roots` 0).
2576const ADOPTED_CONFIG_KEYS: [(&str, &str); 2] = [
2577    ("node_cache_budget", "unlimited"),
2578    ("retention", "keep_all"),
2579];
2580
2581/// Writes the values of [`ADOPTED_CONFIG_KEYS`] into a `config.json` that
2582/// lacks any of them, so a store created before haematite had the key opens on
2583/// upgrade with no operator hand. The open that follows then moves its
2584/// retention onto [`DURABLE_RETENTION`].
2585///
2586/// This crate IS the caller that owns these decisions, and it already makes
2587/// them on every create, so answering them here, once, on upgrade, changes
2588/// nothing about how the store runs and takes the file off the operator's
2589/// desk (the store migrates itself or it doesn't ship).
2590///
2591/// Touches the file in exactly one case: a readable JSON object missing one of
2592/// those keys. Anything else — unreadable bytes, a non-object, an explicit
2593/// value however wrong — is left byte-identical so [`Database::open`] surfaces
2594/// haematite's own sharpest refusal instead of this function guessing over
2595/// it. The write is atomic and durable (tmp file beside the target, fsync,
2596/// rename, directory fsync): a crash mid-migration leaves the original bytes,
2597/// and the next boot migrates again.
2598fn adopt_missing_config_keys(data_dir: &Path, config_file: &Path) -> Result<(), ServerError> {
2599    let Ok(bytes) = std::fs::read(config_file) else {
2600        return Ok(());
2601    };
2602    let Ok(mut config) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
2603        return Ok(());
2604    };
2605    let Some(object) = config.as_object_mut() else {
2606        return Ok(());
2607    };
2608    let mut adopted = Vec::new();
2609    for (key, value) in ADOPTED_CONFIG_KEYS {
2610        if !object.contains_key(key) {
2611            object.insert(key.to_owned(), serde_json::Value::String(value.to_owned()));
2612            adopted.push(key);
2613        }
2614    }
2615    if adopted.is_empty() {
2616        return Ok(());
2617    }
2618    let keys = adopted.join(", ");
2619    let migrated =
2620        serde_json::to_vec_pretty(&config).map_err(|error| ServerError::ConfigValidation {
2621            message: format!(
2622                "durable store config at {} predates `{keys}` and its migration could not be \
2623                 serialized: {error}",
2624                config_file.display()
2625            ),
2626        })?;
2627    let write_error = |step: &str, error: std::io::Error| ServerError::ConfigValidation {
2628        message: format!(
2629            "durable store config at {} predates `{keys}` and migrating it failed at {step}: \
2630             {error}",
2631            config_file.display()
2632        ),
2633    };
2634    let tmp_file = data_dir.join("config.json.migrate-tmp");
2635    let mut tmp = std::fs::File::create(&tmp_file)
2636        .map_err(|error| write_error("creating the temporary file", error))?;
2637    std::io::Write::write_all(&mut tmp, &migrated)
2638        .map_err(|error| write_error("writing the temporary file", error))?;
2639    tmp.sync_all()
2640        .map_err(|error| write_error("syncing the temporary file", error))?;
2641    drop(tmp);
2642    std::fs::rename(&tmp_file, config_file)
2643        .map_err(|error| write_error("renaming over config.json", error))?;
2644    std::fs::File::open(data_dir)
2645        .and_then(|dir| dir.sync_all())
2646        .map_err(|error| write_error("syncing the data directory", error))?;
2647    tracing::warn!(
2648        config = %config_file.display(),
2649        keys = %keys,
2650        "durable store config predates these keys -- wrote the create-arm values (the \
2651         behaviour-preserving ones) so the store opens with no operator action"
2652    );
2653    Ok(())
2654}
2655
2656pub(super) fn reserved_namespace_refusal(channel: &str) -> Option<ServerError> {
2657    if channel.starts_with("liminal.") {
2658        crate::metrics::self_stream_reserved_publication();
2659        Some(ServerError::ReservedNamespace {
2660            channel: channel.to_owned(),
2661        })
2662    } else {
2663        None
2664    }
2665}
2666
2667impl LiminalConnectionServices {
2668    pub(in crate::server) const fn signals(&self) -> &ServerSignals {
2669        &self.signals
2670    }
2671
2672    pub(in crate::server) fn self_stream_head(&self, name: &str) -> Result<u64, ServerError> {
2673        let channels = self.read_channels()?;
2674        let channel = channels
2675            .get(name)
2676            .ok_or_else(|| ServerError::ListenerAccept {
2677                message: format!("self stream '{name}' is absent after append"),
2678            })?;
2679        let heads =
2680            channel
2681                .handle
2682                .durable_positions()
2683                .map_err(|error| ServerError::ListenerAccept {
2684                    message: format!("self stream '{name}' head is unavailable: {error}"),
2685                })?;
2686        drop(channels);
2687        match heads.as_slice() {
2688            [head] => Ok(*head),
2689            _ => Err(ServerError::ListenerAccept {
2690                message: format!("self stream '{name}' must have exactly one durable partition"),
2691            }),
2692        }
2693    }
2694
2695    pub(in crate::server) fn publish_self_stream_batch(
2696        &self,
2697        payloads: &[Vec<u8>],
2698        publisher: &str,
2699    ) -> Result<u64, ServerError> {
2700        if publisher.trim().is_empty() || payloads.is_empty() {
2701            return Err(ServerError::ConfigValidation {
2702                message: "self-stream batch requires a publisher and rows".to_owned(),
2703            });
2704        }
2705        let configured = self
2706            .admit_channel("liminal.events")
2707            .map_err(|error| access_to_server_error(&error))?;
2708        let deliveries = configured
2709            .handle
2710            .publish_batch_with_delivery(
2711                payloads,
2712                &liminal::envelope::PublisherId::new(publisher),
2713                None,
2714            )
2715            .map_err(|error| match error {
2716                liminal::error::LiminalError::PublishUnresolved { append } => {
2717                    ServerError::PublishUnresolved {
2718                        channel: "liminal.events".to_owned(),
2719                        idempotency_key: None,
2720                        sequence: Some(append.sequence()),
2721                        message: append.to_string(),
2722                    }
2723                }
2724                error => ServerError::ListenerAccept {
2725                    message: format!("self-stream receipt batch failed: {error}"),
2726                },
2727            })?;
2728        if deliveries.len() != payloads.len()
2729            || deliveries
2730                .iter()
2731                .any(|delivery| delivery.durable_position().is_none())
2732        {
2733            return Err(ServerError::ListenerAccept {
2734                message: "self-stream receipt batch was refused before append".to_owned(),
2735            });
2736        }
2737        for pair in deliveries.windows(2) {
2738            if pair[0]
2739                .durable_position()
2740                .and_then(|position| position.checked_add(1))
2741                != pair[1].durable_position()
2742            {
2743                return Err(ServerError::ListenerAccept {
2744                    message: "self-stream receipt batch positions are not contiguous".to_owned(),
2745                });
2746            }
2747        }
2748        for delivery in &deliveries {
2749            self.delivered(delivery);
2750        }
2751        deliveries
2752            .last()
2753            .and_then(ChannelDelivery::durable_position)
2754            .and_then(|position| position.checked_add(1))
2755            .ok_or_else(|| ServerError::ListenerAccept {
2756                message: "self-stream receipt batch head is unavailable".to_owned(),
2757            })
2758    }
2759
2760    pub(in crate::server) fn checkpoint_self_stream_heads(
2761        &self,
2762        heads: [u64; 2],
2763    ) -> Result<(), ServerError> {
2764        for (name, head) in ["liminal.events:0", "liminal.cost:0"]
2765            .into_iter()
2766            .zip(heads)
2767        {
2768            block_on(liminal::durability::head_checkpoint::advance_head(
2769                self.durable_store.as_ref(),
2770                name,
2771                head,
2772            ))
2773            .map_err(|error| ServerError::ShutdownFlush {
2774                message: format!("self-stream head checkpoint bridge for '{name}' failed: {error}"),
2775            })?
2776            .map_err(|error| ServerError::ShutdownFlush {
2777                message: format!("self-stream head checkpoint for '{name}' failed: {error}"),
2778            })?;
2779        }
2780        Ok(())
2781    }
2782
2783    pub(in crate::server) fn install_self_stream_channels(&self) -> Result<(), ServerError> {
2784        let names = ["liminal.cost", "liminal.events"];
2785        {
2786            let channels = self.read_channels()?;
2787            for name in names {
2788                if channels.contains_key(name) {
2789                    return Err(ServerError::ConfigValidation {
2790                        message: format!("self-stream channel '{name}' is already configured"),
2791                    });
2792                }
2793            }
2794        }
2795        let mut built = Vec::with_capacity(names.len());
2796        for name in names {
2797            let schema =
2798                resolve_schema_bytes(None).map_err(|error| ServerError::ConfigValidation {
2799                    message: format!("self-stream channel '{name}' schema failed: {error}"),
2800                })?;
2801            let channel = build_configured_channel(
2802                name,
2803                schema,
2804                true,
2805                ChannelOrigin::BootConfigured,
2806                &self.durable_store,
2807                self.cluster.supervisor(),
2808            )
2809            .map_err(|error| ServerError::ConfigValidation {
2810                message: error.boot_message(name),
2811            })?;
2812            built.push((name.to_owned(), Arc::new(channel)));
2813        }
2814        let mut channels =
2815            self.channels
2816                .write()
2817                .map_err(|error| ServerError::ConfigValidation {
2818                    message: format!("self-stream channel roster lock poisoned: {error}"),
2819                })?;
2820        for (name, _) in &built {
2821            if channels.contains_key(name) {
2822                return Err(ServerError::ConfigValidation {
2823                    message: format!("self-stream channel '{name}' was concurrently configured"),
2824                });
2825            }
2826        }
2827        channels.extend(built);
2828        drop(channels);
2829        // SRV-012 R7: every roster channel but `console.*` is a line, and
2830        // these join after the hub's model was built from the roster.
2831        for name in names {
2832            self.console_changed(crate::console::TopologyChange::Channel {
2833                name: name.to_owned(),
2834                life: line_life(true),
2835                state: crate::console::records::LineState::Live,
2836            });
2837        }
2838        Ok(())
2839    }
2840
2841    /// SRV-012 R4: registers `console.topology` as an ephemeral,
2842    /// server-owned channel (never a store write). A channel of that name
2843    /// already configured (ephemeral, since config refuses a durable one) is
2844    /// kept as it is.
2845    pub(in crate::server) fn install_console_channels(&self) -> Result<(), ServerError> {
2846        self.register_console_channel(crate::console::TOPOLOGY_SUBJECT)
2847            .map_err(|error| ServerError::ConfigValidation {
2848                message: format!("console channel registration failed: {error}"),
2849            })
2850    }
2851
2852    /// Puts the ephemeral console channel `name` on the roster unless it is
2853    /// there.
2854    fn register_console_channel(&self, name: &str) -> Result<(), ChannelRegistryError> {
2855        if self.read_roster()?.contains_key(name) {
2856            return Ok(());
2857        }
2858        let schema = resolve_schema_bytes(None).map_err(|error| {
2859            ChannelRegistryError::RosterUnavailable {
2860                message: format!("console channel '{name}' schema failed: {error}"),
2861            }
2862        })?;
2863        let channel = build_configured_channel(
2864            name,
2865            schema,
2866            false,
2867            ChannelOrigin::BootConfigured,
2868            &self.durable_store,
2869            self.cluster.supervisor(),
2870        )
2871        .map_err(|error| error.into_registry_error(name))?;
2872        self.write_roster()?
2873            .entry(name.to_owned())
2874            .or_insert_with(|| Arc::new(channel));
2875        Ok(())
2876    }
2877
2878    /// Publishes on an ordinary channel for an application embedding the server.
2879    ///
2880    /// The live and durable publisher identity is liminal.<host>. Reserved self
2881    /// streams and console channels retain the same refusal as connection publishes.
2882    /// An idempotency key has the same delivery semantics as a connection publish.
2883    ///
2884    /// # Errors
2885    ///
2886    /// Returns a configuration error for an empty or whitespace-only host name,
2887    /// a namespace refusal for a reserved channel, or the existing admission,
2888    /// durability, and publication error.
2889    pub fn publish_embedded(
2890        &self,
2891        host: &str,
2892        channel: &str,
2893        envelope: &MessageEnvelope,
2894        idempotency_key: Option<&str>,
2895    ) -> Result<PublishOutcome, ServerError> {
2896        self.publish_with_origin(
2897            channel,
2898            envelope,
2899            idempotency_key,
2900            PublishOrigin::Host { name: host },
2901        )
2902        .map(|published| published.outcome)
2903    }
2904
2905    pub(in crate::server) fn publish_with_origin(
2906        &self,
2907        channel: &str,
2908        envelope: &MessageEnvelope,
2909        idempotency_key: Option<&str>,
2910        origin: PublishOrigin<'_>,
2911    ) -> Result<OriginPublication, ServerError> {
2912        validate_publication_origin(channel, idempotency_key, origin)?;
2913        // SRV-012 to_stations: only while a console tap is live does the
2914        // fan-out record whom the message reached, in a buffer the tap lends
2915        // and takes back; otherwise the publish is the plain one.
2916        let mut recipients = self.signals.lend_recipients();
2917        // Lane brief ruling 6: a tapped message is first seen as the server
2918        // receives it, before admission; the clock is read only while a tap
2919        // lent a record.
2920        let received_at = recipients.is_some().then(Instant::now);
2921        let result = (|| {
2922            // The roster read holds its guard only long enough to clone the entry's
2923            // `Arc` out; everything below — the dedup bridge and the publish into the
2924            // channel actor — runs with no roster lock held.
2925            //
2926            // This inner admission STAYS even once the connection process consults
2927            // the roster ahead of delegating: this method is public and callable
2928            // without going through a frame at all, and a guard that only exists in
2929            // the caller is not a guard.
2930            let configured = self
2931                .admit_channel(channel)
2932                .map_err(|error| access_to_server_error(&error))?;
2933
2934            // Dedup-on-delivery: a publish carrying an idempotency key is delivered to
2935            // subscribers AT MOST ONCE across re-publishes of the same key. Only a
2936            // fresh `Claimed` decision proceeds to fan-out; a `Completed`/`InFlight`
2937            // decision is a duplicate and is suppressed (no second delivery), which is
2938            // the at-most-once guarantee the aion outbox relies on.
2939            // A1 §5: the namespace is chosen by the channel's DURABILITY, so an
2940            // ephemeral receipt cannot outlive the message it vouches for.
2941            // A keyed publish to a durable channel claims its key WITH the channel
2942            // as evidence and holds the claim across an unresolved append
2943            // (`unresolved.rs`); the ephemeral namespace keeps the claim below.
2944            if let (Some(key), ChannelMode::Durable) =
2945                (idempotency_key, configured.handle.config().mode)
2946            {
2947                return self
2948                    .publish_durable_keyed(
2949                        &configured,
2950                        channel,
2951                        (envelope, origin),
2952                        key,
2953                        recipients.as_mut(),
2954                    )
2955                    .map(OriginPublication::connection);
2956            }
2957            let dedup = self.dedup_for(&configured);
2958            if let Some(key) = idempotency_key {
2959                if !Self::claim_delivery(dedup, key)? {
2960                    // A dedup-suppressed re-publish is still an accepted publish (it is
2961                    // assigned a message id), but it reaches no subscriber, so it counts
2962                    // toward publishes and not deliveries.
2963                    crate::metrics::publish_accepted();
2964                    return Ok(OriginPublication::connection(PublishOutcome {
2965                        message_id: self.next_message_id(),
2966                        delivered: false,
2967                        durable_position: None,
2968                        reach: Reach::Suppressed,
2969                    }));
2970                }
2971            }
2972
2973            let delivery = fan_out(&configured, envelope, origin, recipients.as_mut());
2974            let delivery = match delivery {
2975                Ok(delivery) => delivery,
2976                // An unkeyed durable publish whose append outcome is not known is
2977                // answered as unresolved, never as a definite failure a producer
2978                // would retry into a second row.
2979                Err(liminal::error::LiminalError::PublishUnresolved { append }) => {
2980                    return Err(ServerError::PublishUnresolved {
2981                        channel: channel.to_owned(),
2982                        idempotency_key: None,
2983                        sequence: Some(append.sequence()),
2984                        message: append.to_string(),
2985                    });
2986                }
2987                Err(error) => {
2988                    // The claim above appended an `InFlight` entry but the delivery
2989                    // failed before `complete_receipt` could run. Release the claim so
2990                    // the key is re-claimable; otherwise every re-publish would see
2991                    // `InFlight` and be suppressed forever. Best-effort: surface the
2992                    // ORIGINAL publish error regardless, but never swallow a release
2993                    // failure silently (it leaves the leak intact).
2994                    if let Some(key) = idempotency_key {
2995                        Self::release_claim(dedup, key);
2996                    }
2997                    return Err(ServerError::ListenerAccept {
2998                        message: format!("liminal publish failed for channel '{channel}': {error}"),
2999                    });
3000                }
3001            };
3002
3003            // A1 §5, the two-way dedup mapping. Every publish outcome maps to
3004            // EXACTLY ONE of complete/release — the same shape as the C1 contract:
3005            //
3006            // * **Defer (or Accept) => `complete_receipt`.** The message is
3007            //   admitted and bus-held, so the key is done: a producer retrying it
3008            //   hits `Completed` and there is no duplicate delivery and no InFlight
3009            //   leak. This is the path that makes "re-publishing with the same key
3010            //   is safe and is suppressed while the bus owns the message" true.
3011            // * **Reject => `release_claim`.** Aggregation makes a producer-visible
3012            //   Reject mean delivered-to-zero, so the correct dedup state is "as if
3013            //   never published": tombstone it so a deliberate retry can re-claim.
3014            //   `release_claim` never clobbers a receipt, so the at-most-once guard
3015            //   survives a concurrent completion racing ahead.
3016            //
3017            // Mixed accept/reject reports Defer and so completes — consistent with
3018            // "at least one delivery happened".
3019            if let Some(key) = idempotency_key {
3020                if delivery.is_admitted() {
3021                    block_on(dedup.complete_receipt(key, ProcessingReceipt::new(Vec::new())))
3022                        .map_err(|error| ServerError::ListenerAccept {
3023                            message: format!(
3024                                "dedup receipt bridge failed for key '{key}': {error}"
3025                            ),
3026                        })?
3027                        .map_err(|error| ServerError::ListenerAccept {
3028                            message: format!("dedup receipt write failed for key '{key}': {error}"),
3029                        })?;
3030                } else {
3031                    Self::release_claim(dedup, key);
3032                }
3033            }
3034
3035            let self_row_at = delivery.durable_position().filter(|_| origin.is_server());
3036            Ok(OriginPublication {
3037                outcome: self.delivered(&delivery),
3038                self_row_appended: self_row_at.is_some(),
3039                self_row_at,
3040            })
3041        })();
3042        self.signals.published(
3043            channel,
3044            origin,
3045            envelope,
3046            &result,
3047            recipients.as_ref().zip(received_at),
3048        );
3049        if let Some(buffer) = recipients {
3050            self.signals.give_back(buffer);
3051        }
3052        result
3053    }
3054}
3055
3056/// The library publish of an unkeyed message, recording what it saw in
3057/// `recipients` when the tap lent a record.
3058fn fan_out(
3059    configured: &ConfiguredChannel,
3060    envelope: &MessageEnvelope,
3061    origin: PublishOrigin<'_>,
3062    recipients: Option<&mut liminal::channel::PublishRecord>,
3063) -> Result<liminal::channel::ChannelDelivery, liminal::error::LiminalError> {
3064    let publisher = || publisher_for_origin(origin);
3065    recipients.map_or_else(
3066        || {
3067            configured
3068                .handle
3069                .publish_with_delivery(&envelope.payload, publisher(), None)
3070        },
3071        |recipients| {
3072            configured.handle.publish_recording(
3073                &envelope.payload,
3074                publisher(),
3075                None,
3076                None,
3077                recipients,
3078            )
3079        },
3080    )
3081}
3082
3083fn validate_publication_origin(
3084    channel: &str,
3085    idempotency_key: Option<&str>,
3086    origin: PublishOrigin<'_>,
3087) -> Result<(), ServerError> {
3088    if !origin.is_server() {
3089        refuse_console_namespace(channel).map_err(|error| access_to_server_error(&error))?;
3090        if let Some(error) = reserved_namespace_refusal(channel) {
3091            return Err(error);
3092        }
3093    }
3094    if let PublishOrigin::Host { name } = origin
3095        && name.trim().is_empty()
3096    {
3097        return Err(ServerError::ConfigValidation {
3098            message: "embedded publication requires a nonempty host name".to_owned(),
3099        });
3100    }
3101    if let PublishOrigin::Server { device } = origin {
3102        if device.trim().is_empty() || idempotency_key.is_some() {
3103            return Err(ServerError::ConfigValidation {
3104                message: "server-origin publication requires a device and no idempotency key"
3105                    .to_owned(),
3106            });
3107        }
3108    }
3109    Ok(())
3110}
3111
3112fn publisher_for_origin(origin: PublishOrigin<'_>) -> liminal::envelope::PublisherId {
3113    match origin {
3114        PublishOrigin::Connection { .. } => liminal::envelope::PublisherId::default(),
3115        PublishOrigin::Host { name } => {
3116            liminal::envelope::PublisherId::new(format!("liminal.{name}"))
3117        }
3118        PublishOrigin::Server { device } => {
3119            liminal::envelope::PublisherId::new(format!("liminal.{device}"))
3120        }
3121    }
3122}
3123
3124impl ConnectionServices for LiminalConnectionServices {
3125    fn participant_service(&self) -> Option<InstalledParticipantService> {
3126        self.participant_service.clone()
3127    }
3128
3129    /// The roster's admission decision, exposed across the trait boundary as the
3130    /// permission alone.
3131    ///
3132    /// Two methods share the name `admit_channel` on this type: this trait
3133    /// method, and the private inherent funnel it delegates to. They are the
3134    /// same decision at two different boundaries — the funnel returns the ENTRY
3135    /// the decision was made on, because its in-crate callers go on to publish
3136    /// or subscribe through it, and this one returns `()`, because a caller
3137    /// outside the crate is asking whether it MAY, not for the thing itself.
3138    /// Handing the entry out here would make [`ConfiguredChannel`] public
3139    /// surface and hand a frame-level caller a channel handle it has no business
3140    /// holding. Rust resolves an inherent method ahead of a trait method of the
3141    /// same name, so the funnel's existing callers — and the `Self::` call below
3142    /// — reach the funnel, not this method; the trait form is reached only
3143    /// through a `dyn ConnectionServices`, which is exactly the caller it exists
3144    /// for. `an_admitted_operation_the_service_then_refuses_stays_undifferentiated`
3145    /// is the instrument that would catch this delegation turning into a
3146    /// recursion.
3147    ///
3148    /// `operation` is where the answers part: absence and quiesce refuse
3149    /// publishes and subscribes alike, but the console namespace refuses only
3150    /// a connection's publish (a console reads `console.*` as an ordinary
3151    /// subscriber, and only the server may write there), and a tap target
3152    /// refuses only a subscribe. Both are checked before the roster, so a
3153    /// client is told the same thing whether or not the subject is registered
3154    /// yet.
3155    fn admit_channel(
3156        &self,
3157        operation: ChannelOperation,
3158        channel: &str,
3159    ) -> Result<(), ChannelAccessError> {
3160        match operation {
3161            ChannelOperation::Publish => refuse_console_namespace(channel)?,
3162            ChannelOperation::Subscribe => {
3163                refuse_console_tap_target(channel)?;
3164                self.refuse_console_tap_limit(channel)?;
3165                // SRV-012 R4/R6: a tap channel exists while it has a
3166                // subscriber, and only while the console streams run.
3167                let admitted = if self.signals.console_enabled()
3168                    && super::channel_registry::console_tap_target(channel).is_some()
3169                {
3170                    self.register_console_channel(channel).map_err(|error| {
3171                        ChannelAccessError::RosterUnavailable {
3172                            message: error.to_string(),
3173                        }
3174                    })
3175                } else {
3176                    Ok(())
3177                }
3178                .and_then(|()| Self::admit_channel(self, channel).map(|_| ()));
3179                if admitted.is_err() {
3180                    self.give_back_tap(channel);
3181                }
3182                return admitted;
3183            }
3184        }
3185        Self::admit_channel(self, channel)?;
3186        Ok(())
3187    }
3188
3189    fn publish(
3190        &self,
3191        connection: u64,
3192        channel: &str,
3193        envelope: &MessageEnvelope,
3194        idempotency_key: Option<&str>,
3195    ) -> Result<PublishOutcome, ServerError> {
3196        // The frame path refused this already at admission. This guard is for
3197        // a caller holding the trait directly: a guard that only exists in the
3198        // caller is not a guard.
3199        refuse_console_namespace(channel).map_err(|error| access_to_server_error(&error))?;
3200        self.publish_with_origin(
3201            channel,
3202            envelope,
3203            idempotency_key,
3204            PublishOrigin::Connection { id: connection },
3205        )
3206        .map(|published| published.outcome)
3207    }
3208
3209    fn subscribe(
3210        &self,
3211        channel: &str,
3212        accepted_schemas: &[ProtocolSchemaId],
3213        install: Option<liminal::channel::InboxInstall>,
3214    ) -> Result<ConnectionSubscription, ServerError> {
3215        let subscribed = self.open_subscription(channel, accepted_schemas, install, None);
3216        if subscribed.is_err() {
3217            // SRV-012 R6: a tap slot reserved at admission for a subscribe
3218            // that did not happen is given back, and a tap channel it
3219            // registered with no tap behind it leaves the roster.
3220            self.give_back_tap(channel);
3221        }
3222        subscribed
3223    }
3224
3225    fn unsubscribe(&self, subscription: ConnectionSubscription) -> Result<(), ServerError> {
3226        subscription.unsubscribe()
3227    }
3228
3229    fn open_conversation(
3230        &self,
3231        conversation_id: u64,
3232        subject: &str,
3233    ) -> Result<ConnectionConversation, ServerError> {
3234        self.open_routed_conversation(conversation_id, subject)
3235    }
3236
3237    fn conversation_message(
3238        &self,
3239        conversation: &ConnectionConversation,
3240        envelope: &MessageEnvelope,
3241        op_id: Option<u64>,
3242    ) -> Result<(), ServerError> {
3243        self.routed_conversation_message(conversation, envelope, op_id)
3244    }
3245
3246    fn close_conversation(&self, conversation: ConnectionConversation) -> Result<(), ServerError> {
3247        conversation.close()
3248    }
3249
3250    fn listening(self: Arc<Self>) -> Option<std::thread::JoinHandle<()>> {
3251        self.resolve_claims_behind_listener()
3252    }
3253
3254    fn stop_participant_warm(&self) {
3255        // DUR-020 R5: the warm stops at its next conversation boundary and
3256        // the stop does not wait for it; whatever it had not restored
3257        // restores on the next start. Then the clean stop records the
3258        // capacity snapshot (R3), and the final flush makes it durable.
3259        if let Some(warm) = &self.participant_warm {
3260            warm.stop();
3261            warm.record_clean_stop();
3262        }
3263    }
3264
3265    fn finish_background_store_work(&self) -> Result<(), ServerError> {
3266        self.finish_background_work()
3267    }
3268
3269    fn release_store_holders(&self) -> Result<(), ServerError> {
3270        self.release_holders()
3271    }
3272
3273    fn flush_durable_state(&self) -> Result<(), ServerError> {
3274        // A flush observer publishes to a durable channel, so no channel append lock can be held.
3275        // Separate ephemeral dedup state dies with this incarnation; shared dedup state is covered here.
3276        self.signals.with_flush_barrier(|| {
3277            block_on(self.head_checkpoint_stop.checkpoint_heads())
3278                .map_err(|error| ServerError::ShutdownFlush {
3279                    message: format!("clean-stop head checkpoint bridge failed: {error}"),
3280                })?
3281                .map_err(|error| ServerError::ShutdownFlush {
3282                    message: format!("clean-stop head checkpoint failed: {error}"),
3283                })?;
3284            block_on(self.durable_store.flush())
3285                .map_err(|error| ServerError::ShutdownFlush {
3286                    message: format!("shared durable store flush bridge failed: {error}"),
3287                })?
3288                .map_err(|error| ServerError::ShutdownFlush {
3289                    message: format!("shared durable store flush failed: {error}"),
3290                })?;
3291            Ok(())
3292        })?;
3293        Ok(())
3294    }
3295}
3296
3297/// One roster entry: a channel's library handle, the protocol schema id
3298/// advertised for it, where it came from, and its admission state.
3299#[derive(Debug)]
3300pub(super) struct ConfiguredChannel {
3301    handle: ChannelHandle,
3302    protocol_schema: ProtocolSchemaId,
3303    /// Durable (stored) or ephemeral, as built; the console's line life.
3304    durable: bool,
3305    /// Boot-configured or runtime-registered. Written once at construction and
3306    /// NEVER flipped: it is the only field that predicts what a restart does to
3307    /// this entry, and it defines the population the registration cap counts.
3308    origin: ChannelOrigin,
3309    /// [`STATE_ACTIVE`] or [`STATE_QUIESCED`]. Written at most once, by one
3310    /// `compare_exchange`, so the transition is one-way and exactly one caller
3311    /// can perform it.
3312    state: AtomicU8,
3313    /// The quiesce cause, set STRICTLY BEFORE `state` flips so any reader that
3314    /// observes [`STATE_QUIESCED`] can read it. Written once (the `OnceLock`
3315    /// enforces that structurally), which is why a re-quiesce under a different
3316    /// reason must refuse rather than silently keep or replace one of them.
3317    quiesce_reason: OnceLock<String>,
3318}
3319
3320impl ConfiguredChannel {
3321    /// The entry's admission state as a value.
3322    ///
3323    /// One `Acquire` load and, when quiesced, one read of the already-written
3324    /// reason. Touches no actor: the whole point of recording state on the entry
3325    /// is that observing it cannot spawn the thing being observed.
3326    fn state(&self) -> ChannelState {
3327        if self.state.load(Ordering::Acquire) == STATE_QUIESCED {
3328            ChannelState::Quiesced {
3329                reason: self.recorded_quiesce_reason(),
3330            }
3331        } else {
3332            ChannelState::Active
3333        }
3334    }
3335
3336    /// The recorded quiesce reason, for a caller that has already observed
3337    /// [`STATE_QUIESCED`] with an `Acquire` load.
3338    fn recorded_quiesce_reason(&self) -> String {
3339        self.quiesce_reason
3340            .get()
3341            .map_or_else(|| UNRECORDED_QUIESCE_REASON.to_owned(), Clone::clone)
3342    }
3343
3344    /// Moves this entry from active to quiesced, recording `reason` first.
3345    ///
3346    /// The reason is written to the `OnceLock` BEFORE the `Release`
3347    /// `compare_exchange` that flips the state, so no reader can observe
3348    /// `QUIESCED` without being able to read why. Re-quiescing under the
3349    /// IDENTICAL reason is `Ok(())` — the caller's intent already holds — and a
3350    /// DIFFERENT reason refuses, because the recorded one cannot be replaced
3351    /// without losing a cause and cannot be kept without lying to the caller.
3352    fn quiesce(&self, name: &str, reason: &str) -> Result<(), ChannelRegistryError> {
3353        // Whoever wins the `OnceLock` writes the cause on record; every other
3354        // caller — a re-quiesce or a concurrent racer — is judged against THAT
3355        // reason, never against whether it happened to perform the flip itself.
3356        // Reporting success for having won the CAS would tell a racer whose
3357        // reason lost that its reason took effect.
3358        let recorded = match self.quiesce_reason.set(reason.to_owned()) {
3359            Ok(()) => reason.to_owned(),
3360            Err(_rejected) => self.recorded_quiesce_reason(),
3361        };
3362        // The reason is now readable, so the flip may become visible. The CAS
3363        // result is deliberately unused: the state is `QUIESCED` afterwards
3364        // whether this call or a racer performed the transition, and the answer
3365        // to the caller is governed by the recorded reason above.
3366        let _flipped_here = self.state.compare_exchange(
3367            STATE_ACTIVE,
3368            STATE_QUIESCED,
3369            Ordering::Release,
3370            Ordering::Acquire,
3371        );
3372        if recorded == reason {
3373            return Ok(());
3374        }
3375        Err(ChannelRegistryError::AlreadyQuiesced {
3376            name: name.to_owned(),
3377            reason: recorded,
3378        })
3379    }
3380}
3381
3382#[derive(Debug)]
3383struct LiminalSubscriptionResource {
3384    subscription: liminal::channel::SubscriptionHandle,
3385}
3386
3387impl SubscriptionResource for LiminalSubscriptionResource {
3388    fn try_next_positioned(
3389        &mut self,
3390    ) -> Result<Option<(liminal::envelope::Envelope, Option<u64>)>, ServerError> {
3391        self.subscription
3392            .try_next_positioned()
3393            .map_err(|error| subscription_failure(&error))
3394    }
3395
3396    fn acknowledge_position(&self, position: u64) -> Result<(), ServerError> {
3397        self.subscription
3398            .acknowledge_position(position)
3399            .map_err(|error| subscription_failure(&error))
3400    }
3401
3402    fn consumer_position(&self) -> Result<Option<u64>, ServerError> {
3403        self.subscription
3404            .consumer_position()
3405            .map_err(|error| subscription_failure(&error))
3406    }
3407    fn unsubscribe(self: Box<Self>) -> Result<(), ServerError> {
3408        drop(self.subscription);
3409        Ok(())
3410    }
3411
3412    fn is_overflowed(&self) -> bool {
3413        self.subscription.is_overflowed()
3414    }
3415
3416    fn subscriber_pid(&self) -> Option<u64> {
3417        Some(self.subscription.subscriber_pid())
3418    }
3419
3420    fn has_pending(&self) -> bool {
3421        self.subscription.has_pending()
3422    }
3423
3424    fn try_next(&mut self) -> Option<liminal::envelope::Envelope> {
3425        match self.subscription.try_next() {
3426            Ok(envelope) => envelope,
3427            Err(error) => {
3428                tracing::error!(
3429                    %error,
3430                    "subscription could not receive its next envelope"
3431                );
3432                None
3433            }
3434        }
3435    }
3436}
3437
3438/// Preserves the library's subscription failure at the connection boundary.
3439fn subscription_failure(error: &liminal::error::LiminalError) -> ServerError {
3440    ServerError::ListenerAccept {
3441        message: format!("liminal subscription: {error}"),
3442    }
3443}
3444
3445/// Renders an admission refusal as the service error the trait's callers expect.
3446///
3447/// The two refusals that already existed keep their EXACT bytes: an absent
3448/// channel is still `channel '<name>' is not configured` and a poisoned roster
3449/// is still `channel roster lock poisoned`, so nothing that reads these messages
3450/// today sees a change. Discrimination lives in the reason code
3451/// ([`ChannelAccessError::reason_code`]), which the connection process carries to
3452/// the wire — this rendering is the degraded path for a caller that reaches the
3453/// service directly and has no code to carry.
3454///
3455/// `Quiesced` has no predecessor string to preserve; it carries the refusal's own
3456/// message, reason included.
3457///
3458/// The connection process renders its admission refusals through this SAME
3459/// function rather than through [`ChannelAccessError`]'s own `Display`. Two
3460/// consequences, both wanted. The bytes on the wire for an absent channel stay
3461/// exactly what they have always been, which is the whole of the semver promise
3462/// this lane makes. And an operation refused AT admission and one refused after
3463/// admission by the service produce the identical message, differing only in the
3464/// reason code — so the code really is the only discriminator, instead of the
3465/// message quietly becoming a second one.
3466pub(super) fn access_to_server_error(error: &ChannelAccessError) -> ServerError {
3467    let message = match error {
3468        ChannelAccessError::NotRegistered { name } => {
3469            format!("channel '{name}' is not configured")
3470        }
3471        ChannelAccessError::RosterUnavailable { message } => message.clone(),
3472        ChannelAccessError::Quiesced { .. }
3473        | ChannelAccessError::ConsoleNamespace { .. }
3474        | ChannelAccessError::ConsoleTapTarget { .. }
3475        | ChannelAccessError::ConsoleTapLimit { .. } => error.to_string(),
3476    };
3477    ServerError::ListenerAccept { message }
3478}
3479
3480/// Refuses a subscribe to `console.tap.<target>` whose target is empty or
3481/// under `console.`: the tap never taps the console's own records.
3482///
3483/// # Errors
3484/// Returns [`ChannelAccessError::ConsoleTapTarget`] naming the channel.
3485fn refuse_console_tap_target(channel: &str) -> Result<(), ChannelAccessError> {
3486    use super::channel_registry::{console_tap_target, is_console_subject};
3487    match console_tap_target(channel) {
3488        Some(target) if target.is_empty() || is_console_subject(target) => {
3489            Err(ChannelAccessError::ConsoleTapTarget {
3490                name: channel.to_owned(),
3491            })
3492        }
3493        _ => Ok(()),
3494    }
3495}
3496
3497/// Refuses a connection's publish under `console.`, the server's own namespace.
3498///
3499/// # Errors
3500/// Returns [`ChannelAccessError::ConsoleNamespace`] naming the channel.
3501fn refuse_console_namespace(channel: &str) -> Result<(), ChannelAccessError> {
3502    if super::channel_registry::is_console_subject(channel) {
3503        return Err(ChannelAccessError::ConsoleNamespace {
3504            name: channel.to_owned(),
3505        });
3506    }
3507    Ok(())
3508}
3509
3510pub(super) fn server_error_from_protocol(error: &ProtocolError) -> ServerError {
3511    ServerError::ListenerAccept {
3512        message: format!("protocol operation failed: {error}"),
3513    }
3514}
3515
3516/// The three pinned registration tests. A CHILD module of `services` because
3517/// test 2 must hold the exact entry the admission funnel handed out.
3518#[cfg(test)]
3519#[path = "services_registry_tests.rs"]
3520mod services_registry_tests;
3521
3522#[cfg(test)]
3523#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
3524mod durable_store_tests {
3525    use liminal::durability::open_ephemeral_rooted;
3526
3527    use super::subsystem_census::RecordingSubsystems;
3528    use super::{
3529        ConnectionServices, DEFAULT_SHARD_COUNT, LiminalConnectionServices, SchedulerSubsystem,
3530        build_connection_services, build_connection_services_via, build_durable_store,
3531    };
3532    use crate::ServerError;
3533    use crate::config::types::{LimitsConfig, ServerConfig, ServicesConfig};
3534
3535    /// Counts directory entries under `root`, for the empty/one-dir assertions
3536    /// on an injected ephemeral root.
3537    fn entry_count(root: &std::path::Path) -> usize {
3538        std::fs::read_dir(root)
3539            .expect("ephemeral root is readable")
3540            .count()
3541    }
3542
3543    /// A minimal channel-free config with the given service `profile`. No channels,
3544    /// routing, persistence, or cluster — the shape both profiles accept (the full
3545    /// profile simply builds an empty channel set; the worker-front-door profile
3546    /// requires exactly this shape).
3547    fn config_with_profile(profile: &str) -> ServerConfig {
3548        ServerConfig {
3549            listen_address: "127.0.0.1:0".parse().expect("valid socket addr"),
3550            extra_listen_addresses: Vec::new(),
3551            health_listen_address: "127.0.0.1:1".parse().expect("valid socket addr"),
3552            drain_timeout_ms: 30_000,
3553            channels: Vec::new(),
3554            routing_rules: Vec::new(),
3555            persistence_path: None,
3556            cluster: None,
3557            auth: None,
3558            services: ServicesConfig {
3559                profile: profile.to_owned(),
3560                ..ServicesConfig::default()
3561            },
3562            limits: LimitsConfig::default(),
3563            participant: None,
3564            websocket: None,
3565        }
3566    }
3567
3568    /// §9 D2 front-door construction gate (fs half): building the worker-front-door
3569    /// services creates NO haematite store and NO temp dir, while the full profile
3570    /// over an equally-rooted factory DOES create exactly one store directory.
3571    ///
3572    /// The injected root is the only place an ephemeral store directory can appear
3573    /// (the recording factory roots its stores there), so "the root stays empty on
3574    /// the front-door branch" is a real negative assertion: a regression that gave
3575    /// the front door a store would land its directory here and fail this test. The
3576    /// full-profile arm is the positive control proving the seam genuinely
3577    /// constructs a store when the profile asks for one.
3578    #[test]
3579    fn worker_front_door_builds_no_store_and_no_temp_dir() {
3580        let front_door_root = tempfile::tempdir().expect("test can create an ephemeral root");
3581        let full_root = tempfile::tempdir().expect("test can create an ephemeral root");
3582
3583        let front_door_subsystems = RecordingSubsystems::rooted(front_door_root.path());
3584        let front_door: std::sync::Arc<dyn ConnectionServices> = build_connection_services_via(
3585            &config_with_profile("worker-front-door"),
3586            &front_door_subsystems,
3587        )
3588        .expect("worker-front-door services build");
3589        assert!(
3590            !front_door.supports_channel_operations(),
3591            "the worker front door serves no channel operations"
3592        );
3593        assert_eq!(
3594            entry_count(front_door_root.path()),
3595            0,
3596            "the worker front door creates no ephemeral store directory (no haematite, no temp dir)"
3597        );
3598
3599        let full_subsystems = RecordingSubsystems::rooted(full_root.path());
3600        let mut config = config_with_profile("full");
3601        config.persistence_path = Some(full_root.path().to_path_buf());
3602        let full =
3603            build_connection_services_via(&config, &full_subsystems).expect("full services build");
3604        assert!(
3605            full.supports_channel_operations(),
3606            "full mode serves channel operations"
3607        );
3608        assert_eq!(
3609            entry_count(full_root.path()),
3610            1,
3611            "full mode builds its persistent database in the explicit directory"
3612        );
3613
3614        drop(front_door);
3615        drop(full);
3616    }
3617
3618    /// §9 D2 front-door construction gate (thread half — record-by-construction
3619    /// census): the worker profile constructs NO channel-supervisor,
3620    /// conversation-supervisor, or haematite scheduler, while the SAME instrument
3621    /// over the full profile records all three — the positive control proving the
3622    /// census detects the extra schedulers, so an empty census on the worker branch
3623    /// is a real observation, not a decoration.
3624    ///
3625    /// The instrument's boundary: recording happens INSIDE the [`SubsystemFactory`]
3626    /// methods that are the profile-aware path's only route to these constructors,
3627    /// so a recording cannot be silently omitted — a future subsystem added to this
3628    /// path either goes through the factory (and is recorded by construction) or
3629    /// bypasses it, which is a code-review-visible structural violation of the
3630    /// factory seam, not a silently-missing side call. The connection supervisor's
3631    /// own scheduler is the shared baseline of both profiles and is asserted at the
3632    /// supervisor level (`supervisor::tests`); an OS-level thread census upgrades
3633    /// this when the beamr composition lane's scheduler-inventory API (currently on
3634    /// their branch, not yet consumable from liminal) lands.
3635    #[test]
3636    fn worker_profile_census_is_empty_and_full_profile_records_all_schedulers() {
3637        let worker_root = tempfile::tempdir().expect("test can create an ephemeral root");
3638        let full_root = tempfile::tempdir().expect("test can create an ephemeral root");
3639
3640        let worker_subsystems = RecordingSubsystems::rooted(worker_root.path());
3641        let front_door = build_connection_services_via(
3642            &config_with_profile("worker-front-door"),
3643            &worker_subsystems,
3644        )
3645        .expect("worker-front-door services build");
3646        assert_eq!(
3647            worker_subsystems.recorded(),
3648            Vec::<SchedulerSubsystem>::new(),
3649            "the worker front door constructs no scheduler-owning subsystem"
3650        );
3651
3652        let full_subsystems = RecordingSubsystems::rooted(full_root.path());
3653        let mut config = config_with_profile("full");
3654        config.persistence_path = Some(full_root.path().to_path_buf());
3655        let full =
3656            build_connection_services_via(&config, &full_subsystems).expect("full services build");
3657        assert_eq!(
3658            full_subsystems.recorded(),
3659            vec![
3660                SchedulerSubsystem::ChannelSupervisor,
3661                SchedulerSubsystem::ConversationSupervisor,
3662                SchedulerSubsystem::HaematiteStore,
3663            ],
3664            "the full profile constructs every scheduler-owning subsystem, once each — \
3665             the positive control proving the census instrument detects them"
3666        );
3667
3668        drop(front_door);
3669        drop(full);
3670    }
3671
3672    /// MAJOR-1 regression: the full-only constructors reject a worker-front-door
3673    /// config with a typed `ConfigValidation` error AT ENTRY — no full service can
3674    /// be created through any public config-based constructor under that profile.
3675    #[test]
3676    fn full_only_constructors_reject_worker_profile() {
3677        let config = config_with_profile("worker-front-door");
3678
3679        let from_config = LiminalConnectionServices::from_config(&config);
3680        assert!(
3681            matches!(from_config, Err(ServerError::ConfigValidation { .. })),
3682            "from_config must reject a worker-front-door profile with ConfigValidation, got {from_config:?}"
3683        );
3684
3685        let root = tempfile::tempdir().expect("test can create an ephemeral root");
3686        let store = open_ephemeral_rooted(root.path(), DEFAULT_SHARD_COUNT)
3687            .expect("test store for the rejection check builds");
3688        let from_config_with_store =
3689            LiminalConnectionServices::from_config_with_store(&config, std::sync::Arc::new(store));
3690        assert!(
3691            matches!(
3692                from_config_with_store,
3693                Err(ServerError::ConfigValidation { .. })
3694            ),
3695            "from_config_with_store must reject a worker-front-door profile with ConfigValidation"
3696        );
3697    }
3698
3699    /// MAJOR-1 regression: the profile-aware factory itself re-runs the
3700    /// worker-front-door cross-field checks, so a directly-constructed config (one
3701    /// that never passed file-loading validation) combining the worker profile with
3702    /// full-only machinery is refused with the same typed `ConfigValidation` errors.
3703    #[test]
3704    fn build_connection_services_rejects_worker_profile_with_full_only_fields() {
3705        let mut config = config_with_profile("worker-front-door");
3706        config.channels = vec![crate::config::types::ChannelDef {
3707            name: "orders".to_owned(),
3708            schema_ref: None,
3709            durable: false,
3710            loaded_schema: None,
3711        }];
3712        config.persistence_path = Some(std::path::PathBuf::from("/tmp"));
3713
3714        let result = build_connection_services(&config);
3715        let Err(ServerError::ConfigValidation { message }) = result else {
3716            panic!("expected ConfigValidation for worker profile with full-only fields");
3717        };
3718        assert!(message.contains("builds no channels"), "got: {message}");
3719        assert!(
3720            message.contains("builds no durable store"),
3721            "got: {message}"
3722        );
3723    }
3724
3725    /// The required-path constructor creates the persistent database in its
3726    /// configured directory without populating the separate throwaway root.
3727    #[test]
3728    fn persistent_store_uses_configured_path_and_creates_no_temp_dir() {
3729        let home = tempfile::tempdir().expect("test can create a temp dir");
3730        let ephemeral_root = tempfile::tempdir().expect("test can create an ephemeral root");
3731
3732        let store = build_durable_store(home.path()).expect("persistent store builds");
3733
3734        assert!(
3735            home.path().join("durability").join("config.json").exists(),
3736            "the persistent database is created under the configured path"
3737        );
3738        assert_eq!(
3739            entry_count(ephemeral_root.path()),
3740            0,
3741            "the persistent branch creates no ephemeral guard directory"
3742        );
3743
3744        drop(store);
3745    }
3746
3747    #[test]
3748    fn missing_path_refuses_before_any_store_is_opened() -> Result<(), Box<dyn std::error::Error>> {
3749        let root = tempfile::tempdir()?;
3750        let factory = RecordingSubsystems::rooted(root.path());
3751        let result = build_connection_services_via(&config_with_profile("full"), &factory);
3752        assert!(
3753            matches!(result, Err(ServerError::ConfigValidation { message }) if message.contains("no store is opened"))
3754        );
3755        assert_eq!(entry_count(root.path()), 0);
3756        assert!(factory.recorded().is_empty());
3757        Ok(())
3758    }
3759
3760    /// The store migrates itself: a `config.json` written before haematite
3761    /// required `node_cache_budget` opens on the new server with no operator
3762    /// hand, and the file afterwards carries the key — spelled exactly as the
3763    /// create arm spells it. The pre-field store is fabricated by creating a
3764    /// current one and deleting the key, which is byte-faithful to what a
3765    /// 0.8.1-era store looks like to the parser: the key absent, everything
3766    /// else in place.
3767    #[test]
3768    fn a_store_whose_config_predates_the_budget_field_opens_with_no_operator_hand() {
3769        let home = tempfile::tempdir().expect("test can create a temp dir");
3770        let store = build_durable_store(home.path()).expect("persistent store builds");
3771        drop(store);
3772
3773        let config_file = home.path().join("durability").join("config.json");
3774        let bytes = std::fs::read(&config_file).expect("config.json is readable");
3775        let mut config: serde_json::Value =
3776            serde_json::from_slice(&bytes).expect("config.json parses");
3777        config
3778            .as_object_mut()
3779            .expect("config.json is an object")
3780            .remove("node_cache_budget")
3781            .expect("the created config carries the budget to remove");
3782        std::fs::write(
3783            &config_file,
3784            serde_json::to_vec_pretty(&config).expect("serializes"),
3785        )
3786        .expect("test can age the config");
3787
3788        let reopened = build_durable_store(home.path())
3789            .expect("a pre-budget store opens with no operator action");
3790        drop(reopened);
3791
3792        let after = std::fs::read(&config_file).expect("config.json is readable after");
3793        let migrated: serde_json::Value =
3794            serde_json::from_slice(&after).expect("migrated config parses");
3795        assert_eq!(
3796            migrated.get("node_cache_budget"),
3797            Some(&serde_json::Value::String("unlimited".to_owned())),
3798            "the migrated file carries the budget the create arm chooses"
3799        );
3800    }
3801
3802    /// The migration touches only the one migratable case. A corrupt
3803    /// `config.json` still refuses — through haematite's own parse refusal,
3804    /// which is sharper than anything this crate could say — and the bytes are
3805    /// left exactly as found, so the operator diagnoses the file that failed,
3806    /// not one the migration half-rewrote.
3807    #[test]
3808    fn a_corrupt_config_still_refuses_and_is_left_untouched() {
3809        let home = tempfile::tempdir().expect("test can create a temp dir");
3810        let data_dir = home.path().join("durability");
3811        std::fs::create_dir_all(&data_dir).expect("test can create the data dir");
3812        let config_file = data_dir.join("config.json");
3813        let garbage = b"{ not json at all";
3814        std::fs::write(&config_file, garbage).expect("test can write garbage");
3815
3816        let refused = build_durable_store(home.path());
3817        assert!(refused.is_err(), "a corrupt config refuses to open");
3818        assert_eq!(
3819            std::fs::read(&config_file)
3820                .expect("config.json is still readable")
3821                .as_slice(),
3822            garbage,
3823            "the refusing file is left byte-identical for the operator to read"
3824        );
3825    }
3826
3827    /// A config already carrying the budget is not rewritten: reopening leaves
3828    /// the file byte-identical, so the migration cannot churn mtimes, reorder
3829    /// keys, or otherwise touch a store that needs nothing.
3830    #[test]
3831    fn a_config_already_carrying_the_budget_is_left_byte_identical() {
3832        let home = tempfile::tempdir().expect("test can create a temp dir");
3833        let store = build_durable_store(home.path()).expect("persistent store builds");
3834        drop(store);
3835
3836        let config_file = home.path().join("durability").join("config.json");
3837        let before = std::fs::read(&config_file).expect("config.json is readable");
3838        let reopened = build_durable_store(home.path()).expect("a current store reopens");
3839        drop(reopened);
3840        assert_eq!(
3841            std::fs::read(&config_file).expect("config.json is readable after"),
3842            before,
3843            "a config that already carries the budget is left byte-identical"
3844        );
3845    }
3846
3847    /// A durable store created under `KeepAll` is moved onto reclaim by the
3848    /// server's next open: its `config.json` names `reclaim` afterwards, and the
3849    /// copies its old commits left are gone once reclaim has run.
3850    #[test]
3851    fn a_keep_all_durable_store_reclaims_after_the_next_open() {
3852        let dir = tempfile::tempdir().expect("test can create a store dir");
3853        let data_dir = dir.path().join("durability");
3854        let database = haematite::Database::create(
3855            haematite::DatabaseConfig {
3856                data_dir: data_dir.clone(),
3857                shard_count: 1,
3858                distributed: None,
3859                executor_threads: None,
3860                node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
3861                retention: haematite::Retention::KeepAll,
3862            },
3863            haematite::RuntimeOptions {
3864                long_wait_interval: std::time::Duration::from_secs(5),
3865                reclaim_start: haematite::ReclaimStart::AtOpen,
3866            },
3867        )
3868        .expect("a keep-all store is created");
3869        for round in 0_u32..200 {
3870            let mut value = round.to_be_bytes().to_vec();
3871            value.resize(1_024, 0x5a);
3872            database.put(b"key".to_vec(), value).expect("put");
3873            database.commit().expect("commit");
3874        }
3875        drop(database);
3876        let grown =
3877            haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir.clone()))
3878                .expect("stats of the grown store")
3879                .totals
3880                .enumerated_bytes;
3881
3882        let reopened = super::open_or_create_database(&data_dir).expect("the server opens it");
3883        // The server holds reclaim at open and releases it once its listeners
3884        // bind (DUR-021 R1); the test releases it the same way.
3885        reopened
3886            .release_reclaim()
3887            .expect("the held reclaim is released");
3888        reopened.quiesce_reclaim().expect("reclaim runs to rest");
3889        drop(reopened);
3890
3891        let config: serde_json::Value = serde_json::from_slice(
3892            &std::fs::read(data_dir.join("config.json")).expect("config is readable"),
3893        )
3894        .expect("config is JSON");
3895        assert_eq!(
3896            config["retention"],
3897            serde_json::to_value(super::DURABLE_RETENTION).expect("the retention serializes")
3898        );
3899        let report = haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir))
3900            .expect("stats of the reclaimed store");
3901        assert!(
3902            report.totals.enumerated_bytes <= report.totals.marked_bytes.saturating_mul(3),
3903            "the reopened store holds about its live set: enumerated {} B, marked {} B",
3904            report.totals.enumerated_bytes,
3905            report.totals.marked_bytes
3906        );
3907        assert!(
3908            report.totals.enumerated_bytes.saturating_mul(10) < grown,
3909            "the old copies are gone: {} B now against {grown} B grown",
3910            report.totals.enumerated_bytes
3911        );
3912    }
3913
3914    /// `config.json` exactly as a store grown by an engine that never reclaimed
3915    /// carries it: `reclaim` with `past_roots` 0 is already written, so the
3916    /// open has no retention to adopt (it does move the format to version 3),
3917    /// and every superseded copy is still on disk.
3918    /// `DATA_DIR` stands for the one field the fixture rewrites.
3919    const STORED_RECLAIM_CONFIG: &str = r#"{
3920  "chunking_policy": {
3921    "chunking": "bytes-v2",
3922    "internal_target_bytes": 49152,
3923    "leaf_target_bytes": 65536
3924  },
3925  "data_dir": "DATA_DIR",
3926  "distributed": null,
3927  "executor_threads": null,
3928  "format_version": 2,
3929  "node_cache_budget": "unlimited",
3930  "retention": {
3931    "reclaim": {
3932      "past_roots": 0
3933    }
3934  },
3935  "shard_count": 8
3936}"#;
3937
3938    /// A store whose `config.json` already names reclaim, but whose history was
3939    /// grown with nothing reclaimed, is opened by the server and gives its old
3940    /// copies back with no write and no quiesce. A store with no reclaim
3941    /// record owes a startup pass, and a shard runs it on its first turn. The
3942    /// seed holds both shapes the pass must free: one row overwritten many
3943    /// times, and rows written and then deleted, as a prune leaves them.
3944    #[test]
3945    fn a_stored_reclaim_store_grown_without_reclaim_gives_its_old_copies_back_on_open() {
3946        let dir = tempfile::tempdir().expect("test can create a store dir");
3947        let data_dir = dir.path().join("durability");
3948        let database = haematite::Database::create(
3949            haematite::DatabaseConfig {
3950                data_dir: data_dir.clone(),
3951                shard_count: 8,
3952                distributed: None,
3953                executor_threads: None,
3954                node_cache_budget: Some(haematite::NodeCacheBudget::Unlimited),
3955                retention: haematite::Retention::KeepAll,
3956            },
3957            haematite::RuntimeOptions {
3958                long_wait_interval: std::time::Duration::from_secs(5),
3959                reclaim_start: haematite::ReclaimStart::AtOpen,
3960            },
3961        )
3962        .expect("a keep-all store is created");
3963        for round in 0_u32..200 {
3964            let mut value = round.to_be_bytes().to_vec();
3965            value.resize(1_024, 0x5a);
3966            database.put(b"key".to_vec(), value).expect("put");
3967            database.commit().expect("commit");
3968        }
3969        for row in 0_u32..200 {
3970            let key = [b"pruned-".as_slice(), &row.to_be_bytes()].concat();
3971            database.put(key, vec![0xa5; 1_024]).expect("put");
3972            database.commit().expect("commit");
3973        }
3974        for row in 0_u32..200 {
3975            let key = [b"pruned-".as_slice(), &row.to_be_bytes()].concat();
3976            database.delete(key).expect("delete");
3977        }
3978        database.commit().expect("commit");
3979        drop(database);
3980        let grown_report =
3981            haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir.clone()))
3982                .expect("stats of the grown store");
3983        let grown = grown_report.totals;
3984        assert!(
3985            grown.unmarked_nodes > 0,
3986            "the seed leaves copies no root reaches"
3987        );
3988        let mut owed: std::collections::BTreeSet<usize> = grown_report
3989            .shards
3990            .iter()
3991            .filter(|shard| shard.nodes.unmarked_nodes > 0)
3992            .map(|shard| shard.shard_id)
3993            .collect();
3994
3995        let config_file = data_dir.join("config.json");
3996        let seeded = STORED_RECLAIM_CONFIG.replace(
3997            "DATA_DIR",
3998            data_dir.to_str().expect("the test dir is UTF-8"),
3999        );
4000        std::fs::write(&config_file, &seeded).expect("config.json is written");
4001
4002        let reopened = super::open_or_create_database(&data_dir).expect("the server opens it");
4003        let watch = reopened
4004            .watch_reclaim_passes(64)
4005            .expect("every shard accepts a pass watch");
4006        // Released as the server releases it once its listeners bind (DUR-021 R1).
4007        reopened
4008            .release_reclaim()
4009            .expect("the held reclaim is released");
4010        let mut unmarked_by_shard = std::collections::BTreeMap::new();
4011        while !owed.is_empty() {
4012            let report = watch
4013                .recv_timeout(std::time::Duration::from_secs(30))
4014                .expect("each pass the open owes ends without a write or a quiesce");
4015            assert_eq!(
4016                report.outcome,
4017                haematite::ReclaimPassOutcome::Passed,
4018                "the pass the open owed on shard {} finished",
4019                report.shard_id
4020            );
4021            if owed.remove(&report.shard_id) {
4022                unmarked_by_shard.insert(report.shard_id, report.unmarked_bytes);
4023            }
4024        }
4025        drop(reopened);
4026
4027        let after = haematite::db::vacuum::stats(&haematite::db::vacuum::Options::new(data_dir))
4028            .expect("stats of the reclaimed store")
4029            .totals;
4030        eprintln!(
4031            "grown {} B, after {} B, {} unreachable nodes seeded, unmarked bytes by shard {unmarked_by_shard:?}",
4032            grown.enumerated_bytes, after.enumerated_bytes, grown.unmarked_nodes
4033        );
4034        assert!(
4035            after.enumerated_bytes.saturating_mul(10) < grown.enumerated_bytes,
4036            "the old copies are gone: {} B now against {} B grown",
4037            after.enumerated_bytes,
4038            grown.enumerated_bytes
4039        );
4040        let config: serde_json::Value = serde_json::from_slice(
4041            &std::fs::read(&config_file).expect("config.json is readable after"),
4042        )
4043        .expect("config is JSON");
4044        assert_eq!(
4045            config["retention"],
4046            serde_json::to_value(super::DURABLE_RETENTION).expect("the retention serializes"),
4047            "config.json names reclaim after the open"
4048        );
4049    }
4050}