Skip to main content

liminal_server/server/connection/
services.rs

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