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