Skip to main content

liminal_server/server/connection/
services.rs

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