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