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