Skip to main content

liminal_sdk/remote/
participant.rs

1//! Remote participant state, durable client records, and typed transport outcomes.
2//!
3//! This module owns process mechanics only. Every participant lifecycle and
4//! correlation decision is delegated to `liminal-protocol`; the SDK stores the
5//! crate aggregate and its sealed one-use authorities without mirroring their
6//! rules.
7
8mod recovery;
9mod replay_apply;
10
11pub use recovery::{
12    CredentialAttachReissueReason, LostCredentialAttachRefusalReason,
13    RemoteCredentialAttachRecovery, RemoteDetachReplayOutcome, RemoteExpectedOperationRecovery,
14    RemoteLostOperationResolution, RemoteLostReconnectResolution, RemoteReconnectAttemptOutcome,
15    RemoteReconnectPermitRecovery, RemoteReplayApplyOutcome, RemoteTransportLossOutcome,
16};
17
18use alloc::sync::Arc;
19use core::fmt;
20use core::time::Duration;
21
22use liminal_protocol::client::{
23    ClientCorrelatedInboundDecision, ClientInboundDecision, ClientInboundRefusalReason,
24    ClientOperationRecordDecision, ClientOperationRecordRefusalReason, ClientParticipantAggregate,
25    ClientResponseCorrelation, ClientResumeRecord, ClientResumeRecordDecodeError,
26    ClientResumeRecordEncodeError, ClientResumeRestoreError, ExpectedOperationFateRefusalReason,
27    ExpectedOperationTransportFate, ExpectedParticipantOperation, ReconnectPermitDecision,
28    decide_correlated_inbound, decide_inbound, record_expected_operation_fate,
29    record_transport_fate,
30};
31use liminal_protocol::outcome::ReconnectDelayResult;
32use liminal_protocol::wire::{
33    ClientRequest, DeliverySeq, ParticipantFrame, RecordAdmissionEnvelope,
34    RecordAdmissionFaultClass, ServerPush, ServerValue, ValidatedFrameLimit, encoded_len,
35};
36use spin::Mutex;
37
38use crate::SdkError;
39
40use super::protocol::{ParticipantTransportFrame, RemoteTransport};
41use super::{RemoteConfig, ServerAddress};
42
43/// Storage boundary for canonical `LPCR` client resume bytes.
44///
45/// Implementations must replace the previously committed bytes durably before
46/// returning `Ok(())`. The SDK calls this boundary after the protocol crate's
47/// commit seal and before releasing executable operation authority.
48pub trait ParticipantResumeStore: Send {
49    /// Durably replaces the stored canonical client resume record.
50    ///
51    /// # Errors
52    ///
53    /// Returns [`SdkError::Store`] when the bytes were not durably committed.
54    fn persist(&mut self, canonical_lpcr: &[u8]) -> Result<(), SdkError>;
55}
56
57/// What a participant handle's quiet drains have done on its current
58/// connection, read without a lock and without touching the socket.
59///
60/// [`try_receive`](RemoteParticipantHandle::try_receive) ends a quiet drain on
61/// a transport fence, never on elapsed silence, so on a healthy connection
62/// `reads_timed_out` stays at zero however many pumps run. A read window that
63/// closes before the fence's reply arrives is counted there. Both counts start
64/// at zero when a connection is established and are not carried across a
65/// reconnect.
66#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
67pub struct ParticipantPumpDiagnostics {
68    /// Quiet drains run: each call to `try_receive` that reached the transport.
69    pub pumps_run: u64,
70    /// Socket reads inside those drains that ended because their read window
71    /// closed with nothing to read.
72    pub reads_timed_out: u64,
73}
74
75/// Transport-layer testimony identifying the connection attempt that delivered a frame.
76///
77/// This is the sealed transport context anticipated by rationale 15 in
78/// `LP-CLIENT-GOAL`. It does not alter the wire format or relax the protocol
79/// crate's conservative `RecordAdmission` ambiguity.
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub struct ParticipantResponseProvenance {
82    connection_id: u64,
83    attempt_id: u64,
84}
85
86impl ParticipantResponseProvenance {
87    #[cfg(feature = "std")]
88    pub(super) const fn new(connection_id: u64, attempt_id: u64) -> Self {
89        Self {
90            connection_id,
91            attempt_id,
92        }
93    }
94
95    /// Returns the local identity of the established socket.
96    #[must_use]
97    pub const fn connection_id(self) -> u64 {
98        self.connection_id
99    }
100
101    /// Returns the local identity of the real connection attempt.
102    #[must_use]
103    pub const fn attempt_id(self) -> u64 {
104        self.attempt_id
105    }
106}
107
108/// Failure at the SDK participant state, codec, storage, or transport boundary.
109#[derive(Debug, thiserror::Error)]
110pub enum RemoteParticipantError {
111    /// A prior commit could not be persisted, so no aggregate authority remains reachable.
112    ///
113    /// `source` is the RETAINED cause: the exact [`SdkError`] the caller's own
114    /// store returned at the seam that bricked the handle. It travels on the
115    /// error itself, so a caller who merely CAUGHT this value -- through a `?`,
116    /// across a channel, out of a join -- can attribute the hold by matching a
117    /// typed value rather than parsing rendered text. It is also this error's
118    /// [`source`](core::error::Error::source).
119    ///
120    /// A `None` means the aggregate is unreachable for a reason that did not
121    /// originate in the store. That discrimination is why the field is an
122    /// `Option` and not an `SdkError`: there is no store failure to name, so
123    /// [`source`](core::error::Error::source) reports `None` rather than
124    /// inventing a link, and the rendered message drops the durability clause it
125    /// would otherwise be asserting without evidence.
126    #[error(
127        "participant state is unavailable{}",
128        source.as_ref().map_or("", |_| " after an unreleased durability failure")
129    )]
130    StateUnavailable {
131        /// The store failure that made the aggregate unreachable, when that is why.
132        #[source]
133        source: Option<SdkError>,
134    },
135    /// The protocol crate could not encode the current aggregate as canonical LPCR.
136    #[error("client resume record encode failed: {0:?}")]
137    ResumeEncode(ClientResumeRecordEncodeError),
138    /// Persisted bytes were not a canonical LPCR record.
139    #[error("client resume record decode failed: {0:?}")]
140    ResumeDecode(ClientResumeRecordDecodeError),
141    /// Canonical facts violated a protocol restore invariant.
142    #[error("client resume record restore failed: {0:?}")]
143    ResumeRestore(ClientResumeRestoreError),
144    /// The caller-owned durable store rejected a canonical record.
145    ///
146    /// The `SdkError` is exposed as this error's [`source`](core::error::Error::source)
147    /// so a caller can walk to the typed store failure instead of matching on
148    /// rendered text.
149    #[error("client resume record persistence failed: {0}")]
150    Storage(#[source] SdkError),
151    /// The real transport failed outside a typed fate-reporting operation.
152    ///
153    /// The `SdkError` is exposed as this error's [`source`](core::error::Error::source),
154    /// for the same reason as [`Storage`](RemoteParticipantError::Storage).
155    #[error("participant transport failed: {0}")]
156    Transport(#[source] SdkError),
157    /// A client-to-server request appeared on the SDK receive side.
158    #[error("participant transport decoded a request in the client receive direction")]
159    InvalidInboundDirection,
160    /// No live response correlation exists for a replay-specific input.
161    #[error("no live participant response authority is held")]
162    ResponseAuthorityUnavailable,
163    /// A declared participant frame limit was outside the protocol's bounds.
164    #[error("declared participant frame limit {max_frame_bytes} is outside the protocol's bounds")]
165    InvalidFrameLimit {
166        /// The limit as declared.
167        max_frame_bytes: u64,
168    },
169    /// The request's complete participant frame exceeds the declared frame limit.
170    ///
171    /// Reported by [`record_operation`](RemoteParticipantHandle::record_operation)
172    /// BEFORE any write-ahead authority is spent or any byte reaches the wire, so
173    /// the operation that follows is not refused as outstanding. The request comes
174    /// back unchanged. Without a declared limit the broker's transport gate would
175    /// answer such a frame with a pre-semantic `ParticipantTransportRejected`
176    /// (`FrameTooLarge`), which is never an answer to the operation and leaves it
177    /// outstanding forever (manifold #session.feed, 2026-09-11 12:57Z).
178    #[error(
179        "request frame of {complete_frame_bytes} bytes exceeds the declared participant frame limit of {max_frame_bytes} bytes"
180    )]
181    RequestExceedsFrameLimit {
182        /// The request, returned unchanged; nothing was recorded or sent.
183        request: ClientRequest,
184        /// Header plus payload bytes of the frame the request would have made.
185        complete_frame_bytes: u64,
186        /// The declared limit it exceeds.
187        max_frame_bytes: u64,
188    },
189}
190
191/// Opaque one-use operation released only after the SDK persisted sealed LPCR bytes.
192#[derive(Debug)]
193pub struct RemoteParticipantOperation {
194    operation: ExpectedParticipantOperation,
195    durability: OperationDurability,
196}
197
198#[derive(Clone, Copy, Debug, PartialEq, Eq)]
199enum OperationDurability {
200    WriteAhead,
201    Continuous,
202}
203
204/// Result of admitting an outbound request through the crate write-ahead barrier.
205#[derive(Debug)]
206pub enum RemoteOperationRecordOutcome {
207    /// Canonical LPCR bytes were persisted and one operation may now be sent.
208    Recorded(RemoteParticipantOperation),
209    /// A continuous acknowledgement bypassed the write-ahead slot by crate rule.
210    Continuous(RemoteParticipantOperation),
211    /// The crate refused the exact request without changing aggregate state.
212    Refused {
213        /// Exact refused request.
214        request: ClientRequest,
215        /// Closed protocol refusal reason.
216        reason: ClientOperationRecordRefusalReason,
217    },
218}
219
220/// Typed operation-domain consequence of an established transport loss.
221#[derive(Debug, PartialEq, Eq)]
222pub enum RemoteOperationTransportFate {
223    /// A non-detach operation's response became unavailable.
224    Recorded {
225        /// Exact terminalized request.
226        request: ClientRequest,
227    },
228    /// The exact detach was returned to parked replay.
229    DetachParked,
230    /// The crate retained the live correlation unchanged.
231    Refused {
232        /// Closed refusal reason from the generic operation-fate gate.
233        reason: ExpectedOperationFateRefusalReason,
234    },
235    /// No response authority was outstanding.
236    NotOutstanding,
237}
238
239/// Typed reconnect permit returned by a crate-authorized fresh event.
240#[derive(Debug)]
241pub struct RemoteReconnectPermit {
242    pub(super) permit: liminal_protocol::client::ReconnectAttemptPermit,
243}
244
245/// Event-driven reconnect permit decision; there is no delay or timer arm.
246#[derive(Debug)]
247pub enum RemoteReconnectPermitOutcome {
248    /// The crate minted one one-use permit.
249    Permitted {
250        /// Opaque permit for one real connection attempt.
251        permit: RemoteReconnectPermit,
252        /// Legacy-named crate result whose value is event-only.
253        result: ReconnectDelayResult,
254    },
255    /// Existing authority was retained.
256    Refused {
257        /// Closed crate refusal reason.
258        reason: liminal_protocol::client::ReconnectPermitRefusalReason,
259        /// Event-only crate result.
260        result: ReconnectDelayResult,
261    },
262}
263
264/// Result of sending an operation on the participant transport.
265#[derive(Debug)]
266pub enum RemoteParticipantSendOutcome {
267    /// The request bytes were written on this connection attempt.
268    Sent {
269        /// Sealed transport context for a later response.
270        provenance: ParticipantResponseProvenance,
271    },
272    /// The write failed and both operation and reconnect fates were delegated.
273    TransportLost {
274        /// Concrete socket failure.
275        error: SdkError,
276        /// Crate-owned operation-fate result.
277        operation_fate: RemoteOperationTransportFate,
278        /// Crate-owned reconnect permit result.
279        reconnect: RemoteReconnectPermitOutcome,
280    },
281}
282
283/// Liveness bound for [`RemoteParticipantHandle::try_receive`]'s transport probe.
284///
285/// A healthy quiet drain ends on the broker's Pong, not this duration. An
286/// unanswered probe reaches the bound as a named transport failure, never a
287/// quiet result. The value remains the existing transport read window; explicit
288/// `receive_within` budgets and the reply-owed response deadline are unchanged.
289pub const PARTICIPANT_PUMP_WINDOW: Duration = super::IO_TIMEOUT;
290
291/// Typed result of one decoded participant frame on the real receive path.
292#[derive(Debug)]
293pub enum RemoteParticipantInbound {
294    /// The protocol crate correlated and applied a semantic response.
295    Applied {
296        /// Exact applied server value.
297        value: ServerValue,
298        /// Connection/attempt that delivered it.
299        provenance: ParticipantResponseProvenance,
300    },
301    /// The protocol crate retained the response and aggregate unchanged.
302    Refused {
303        /// Exact refused server value.
304        value: ServerValue,
305        /// Closed crate refusal reason, including conservative ambiguity.
306        reason: ClientInboundRefusalReason,
307        /// Connection/attempt that delivered it.
308        provenance: ParticipantResponseProvenance,
309    },
310    /// The server could not settle the request's durable write
311    /// (`ServerValue::WriteOutcomeUnknown`, 0x0128): its answer is lost, not
312    /// refused. The operation's expectation has ended as it does on a dropped
313    /// transport, and its response correlation is retired, so nothing waits on
314    /// an answer the server has already given. `operation_fate` says how it
315    /// ended: [`Recorded`](RemoteOperationTransportFate::Recorded) hands back
316    /// the exact request to re-present with its token,
317    /// [`DetachParked`](RemoteOperationTransportFate::DetachParked) returns a
318    /// detach to its parked replay, and
319    /// [`Refused`](RemoteOperationTransportFate::Refused) names why the crate
320    /// would not end it.
321    OutcomeUnknown {
322        /// Exact server value (`ServerValue::WriteOutcomeUnknown`).
323        value: ServerValue,
324        /// How the operation's expectation ended.
325        operation_fate: RemoteOperationTransportFate,
326        /// Connection/attempt that delivered it.
327        provenance: ParticipantResponseProvenance,
328    },
329    /// Server push decoded in the client direction; no correlation rule applies.
330    Push {
331        /// Exact pushed value.
332        value: ServerPush,
333        /// Connection/attempt that delivered it.
334        provenance: ParticipantResponseProvenance,
335    },
336}
337
338/// TERMINAL fate of one ordinary record admission, read off the server's own
339/// answer.
340///
341/// ## The contract this exists to state
342///
343/// A carrier that write-aheads its records needs exactly one bit from every
344/// inbound: MAY I CLEAR THE SLOT? Before this type there was no way to ask it.
345/// A server refusal arrived as `RemoteParticipantInbound::Applied { value }` —
346/// an `Ok`, type-indistinguishable from a commit — and
347/// [`committed_delivery_seq`](RemoteParticipantInbound::committed_delivery_seq)
348/// folded every refusal into the same `None` as "not a record answer at all".
349/// So a carrier could only distinguish them by matching the wire enum itself,
350/// and the 2026-08-28 field carrier did the safe thing instead: it treated
351/// everything that was not a commit as unknown, kept the slot, and retried
352/// forever.
353///
354/// Terminal means: the answer will not change. Clear the write-ahead slot.
355/// [`Committed`](Self::Committed) clears it because the record is durable;
356/// [`ProtocolFault`](Self::ProtocolFault) clears it because re-presenting the
357/// same bytes will be refused identically on every boot, so the retry is not a
358/// recovery, it is the incident.
359///
360/// Everything that is NOT terminal keeps the slot, and it stays outside this
361/// type by construction rather than by a variant a caller could misread:
362///
363/// - **Transient refusals** (`ObserverBackpressure`,
364///   `MarkerSettlementBackpressure`, and the rest) are still
365///   [`Applied`](RemoteParticipantInbound::Applied) values, and
366///   [`record_admission_fate`](RemoteParticipantInbound::record_admission_fate)
367///   answers `None` for them. Their whole `ServerValue` remains available on
368///   the inbound for a caller that wants to schedule its retry.
369/// - **Connection loss** never produces a `RemoteParticipantInbound` at all: it
370///   surfaces on the send side as
371///   [`RemoteParticipantSendOutcome::TransportLost`] and on the receive side as
372///   `Err(RemoteParticipantError::Transport(..))`. Fate-unknown is a different
373///   `Result` position, not a fate.
374/// - **A write the server could not settle** arrives as
375///   [`OutcomeUnknown`](RemoteParticipantInbound::OutcomeUnknown), not
376///   `Applied`, so `record_admission_fate` never sees it: its
377///   `operation_fate` is the same lost-response fate a dropped transport
378///   gives, and a `Recorded` fate hands back the request to re-present.
379#[derive(Clone, Debug, PartialEq, Eq)]
380pub enum RecordAdmissionFate {
381    /// The record is durable at this delivery sequence.
382    Committed {
383        /// Echoed common request envelope of the admitted record.
384        request: RecordAdmissionEnvelope,
385        /// Sequence the server assigned.
386        delivery_seq: DeliverySeq,
387    },
388    /// The spine named an internal protocol fault and terminally refused.
389    ///
390    /// The class is coarse on purpose — the fault's full detail stays in the
391    /// SERVER's log. What reaches here is enough to dead-letter the record and
392    /// raise an operator signal, which is the only correct response.
393    ProtocolFault {
394        /// Echoed common request envelope of the refused record.
395        request: RecordAdmissionEnvelope,
396        /// Coarse class of the fault the spine named.
397        class: RecordAdmissionFaultClass,
398    },
399}
400
401impl RecordAdmissionFate {
402    /// Borrows the echoed request envelope, whichever fate this is.
403    #[must_use]
404    pub const fn request(&self) -> &RecordAdmissionEnvelope {
405        match self {
406            Self::Committed { request, .. } | Self::ProtocolFault { request, .. } => request,
407        }
408    }
409
410    /// Whether the record reached durability.
411    ///
412    /// Both variants are terminal — that is what makes this type the answer to
413    /// "may I clear the slot?" — so `is_committed` is deliberately the question
414    /// asked here, and terminality is carried by the `Option` around the fate
415    /// rather than by a predicate that would always be `true`.
416    #[must_use]
417    pub const fn is_committed(&self) -> bool {
418        matches!(self, Self::Committed { .. })
419    }
420}
421
422impl RemoteParticipantInbound {
423    /// The TERMINAL fate of a record admission, when this inbound settles one.
424    ///
425    /// `Some` exactly for an APPLIED commit or an APPLIED terminal protocol
426    /// fault. `None` for every transient refusal, every value answering another
427    /// operation, every [`Push`](Self::Push), and — deliberately — for every
428    /// [`Refused`](Self::Refused): the crate declined to correlate that value,
429    /// so it settles nothing and the carrier's slot stays.
430    ///
431    /// Transport-agnostic by construction. Both the wire and the in-process
432    /// loopback transport decode through the same codec and land in the same
433    /// `apply_inbound`, so a carrier written against this reads the same fate
434    /// either way.
435    #[must_use]
436    pub fn record_admission_fate(&self) -> Option<RecordAdmissionFate> {
437        let Self::Applied { value, .. } = self else {
438            return None;
439        };
440        match value {
441            ServerValue::RecordCommitted(committed) => Some(RecordAdmissionFate::Committed {
442                request: committed.request().clone(),
443                delivery_seq: committed.delivery_seq(),
444            }),
445            ServerValue::RecordAdmissionProtocolFault(fault) => {
446                Some(RecordAdmissionFate::ProtocolFault {
447                    request: fault.request.clone(),
448                    class: fault.class,
449                })
450            }
451            _ => None,
452        }
453    }
454
455    /// The record sequence the server assigned an admitted record, when this
456    /// inbound is an APPLIED [`ServerValue::RecordCommitted`].
457    ///
458    /// This is the answer to a `RecordAdmission`, read off the exact wire value
459    /// the protocol crate applied. It saves every caller destructuring the wire
460    /// enum to reach the one number a record admission is asked for, without
461    /// removing that value: [`Applied`](Self::Applied) still carries the whole
462    /// `ServerValue`, so this is purely additive.
463    ///
464    /// `None` for everything else, and that includes a `RecordCommitted` the
465    /// crate REFUSED. A refused commit carries a sequence on the wire while
466    /// leaving the aggregate and its correlation untouched -- returning it here
467    /// would report a commitment the crate deliberately declined to make. It is
468    /// also `None` for a [`Push`](Self::Push), which is a delivery rather than
469    /// a correlated response, and for an [`OutcomeUnknown`](Self::OutcomeUnknown),
470    /// whose write the server could not settle.
471    #[must_use]
472    pub const fn committed_delivery_seq(&self) -> Option<DeliverySeq> {
473        match self {
474            Self::Applied {
475                value: ServerValue::RecordCommitted(committed),
476                ..
477            } => Some(committed.delivery_seq()),
478            Self::Applied { .. }
479            | Self::OutcomeUnknown { .. }
480            | Self::Refused { .. }
481            | Self::Push { .. } => None,
482        }
483    }
484}
485
486pub(super) struct RemoteParticipantState<S> {
487    pub(super) aggregate: Option<ClientParticipantAggregate>,
488    pub(super) correlation: Option<ClientResponseCorrelation>,
489    pub(super) reconnect_attempt: Option<liminal_protocol::client::ReconnectInProgressAttempt>,
490    /// The caller-declared complete-frame limit every outbound request is
491    /// measured against before it is recorded. `None` declares nothing and
492    /// leaves the broker's transport gate as the only bound.
493    pub(super) frame_limit: Option<ValidatedFrameLimit>,
494    pub(super) store: S,
495    /// The store failure that made the aggregate unreachable, if that is why.
496    ///
497    /// The failure itself is returned to whoever made the failing call and to
498    /// nobody else. This retains it so the question "why is this handle dead"
499    /// has a typed answer for every later caller: `take_aggregate` reads it back
500    /// out of here onto the
501    /// [`StateUnavailable`](RemoteParticipantError::StateUnavailable) it reports,
502    /// and [`unavailability_cause`](RemoteParticipantHandle::unavailability_cause)
503    /// answers it without a failing call.
504    pub(super) unavailable: Option<SdkError>,
505}
506
507impl<S> RemoteParticipantState<S> {
508    /// Records a store failure as the reason the aggregate is about to become
509    /// unreachable, and names that failure for the caller.
510    ///
511    /// Every seam that drops the aggregate on a store failure goes through here,
512    /// so the retained cause cannot drift from the returned one: they are the
513    /// same value.
514    pub(super) fn brick(&mut self, error: SdkError) -> RemoteParticipantError {
515        self.unavailable = Some(error.clone());
516        RemoteParticipantError::Storage(error)
517    }
518}
519
520/// Remote participant entrypoint backed by protocol-crate state and canonical LPCR storage.
521///
522/// Records are deliberately not promised as generally successful: the reduced-B1
523/// server surface fails fully authorized `RecordAdmission` and `Leave` closed until
524/// live claim-frontier acquisition lands (`docs/design/LP-GAP-CLOSURE-GOAL.md:145`).
525pub struct RemoteParticipantHandle<S> {
526    pub(super) server_address: ServerAddress,
527    pub(super) transport: Arc<dyn RemoteTransport>,
528    pub(super) state: Mutex<RemoteParticipantState<S>>,
529}
530
531impl<S> fmt::Debug for RemoteParticipantHandle<S> {
532    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
533        formatter
534            .debug_struct("RemoteParticipantHandle")
535            .field("server_address", &self.server_address)
536            .finish_non_exhaustive()
537    }
538}
539
540impl<S: ParticipantResumeStore> RemoteParticipantHandle<S> {
541    /// Creates and durably checkpoints a fresh unbound participant.
542    ///
543    /// # Errors
544    ///
545    /// Returns a typed encode or storage error before the handle is exposed.
546    pub fn new(config: &RemoteConfig, store: S) -> Result<Self, RemoteParticipantError> {
547        Self::from_aggregate(config, store, ClientParticipantAggregate::new())
548    }
549
550    /// Decodes, validates, restores, and durably records crash testimony before exposure.
551    ///
552    /// # Errors
553    ///
554    /// Returns typed LPCR decode/restore, encode, or storage errors.
555    pub fn restore(
556        config: &RemoteConfig,
557        store: S,
558        canonical_lpcr: &[u8],
559    ) -> Result<Self, RemoteParticipantError> {
560        let record = ClientResumeRecord::decode_canonical(canonical_lpcr)
561            .map_err(RemoteParticipantError::ResumeDecode)?;
562        let aggregate = record
563            .restore()
564            .map_err(RemoteParticipantError::ResumeRestore)?;
565        Self::from_aggregate(config, store, aggregate)
566    }
567
568    fn from_aggregate(
569        config: &RemoteConfig,
570        mut store: S,
571        aggregate: ClientParticipantAggregate,
572    ) -> Result<Self, RemoteParticipantError> {
573        persist(&mut store, &aggregate)?;
574        Ok(Self {
575            server_address: config.server_address.clone(),
576            transport: Arc::clone(&config.transport),
577            state: Mutex::new(RemoteParticipantState {
578                aggregate: Some(aggregate),
579                correlation: None,
580                reconnect_attempt: None,
581                frame_limit: None,
582                store,
583                unavailable: None,
584            }),
585        })
586    }
587
588    /// Returns the store failure that made this handle unavailable, if that is
589    /// why it is unavailable.
590    ///
591    /// A `Some` names the exact [`SdkError`] the caller's own store returned, so
592    /// a hold can be attributed by matching a typed value. A `None` means the
593    /// handle is either live or unavailable for a reason that did not originate
594    /// in the store — which is itself the discrimination this exists to provide.
595    ///
596    /// The same retained value is carried on
597    /// [`StateUnavailable`](RemoteParticipantError::StateUnavailable) and is that
598    /// error's [`source`](core::error::Error::source), so a caller who CAUGHT
599    /// that error already holds the cause and does not need this. This accessor
600    /// answers the other question — one asked of a handle you still HOLD, rather
601    /// than of an error you caught — and it answers it without making a call
602    /// that must fail in order to find out. Its `None` therefore covers one case
603    /// the variant's `None` cannot: a handle that is simply still live.
604    #[must_use]
605    pub fn unavailability_cause(&self) -> Option<SdkError> {
606        self.state.lock().unavailable.clone()
607    }
608
609    /// Runs `record_operation -> commit -> LPCR persist -> into_parts` exactly.
610    ///
611    /// # Errors
612    ///
613    /// Returns typed resume encoding or storage failures. A failed post-commit
614    /// persistence leaves the handle unavailable and releases no authority.
615    pub fn record_operation(
616        &self,
617        request: ClientRequest,
618    ) -> Result<RemoteOperationRecordOutcome, RemoteParticipantError> {
619        let mut state = self.state.lock();
620        if let Some(limit) = state.frame_limit {
621            let complete_frame_bytes = request_frame_bytes(&request)?;
622            if complete_frame_bytes > limit.get() {
623                return Err(RemoteParticipantError::RequestExceedsFrameLimit {
624                    request,
625                    complete_frame_bytes,
626                    max_frame_bytes: limit.get(),
627                });
628            }
629        }
630        let aggregate = take_aggregate(&mut state)?;
631        match liminal_protocol::client::record_operation(aggregate, request) {
632            ClientOperationRecordDecision::Pending(pending) => {
633                let commit = pending.commit();
634                let record = commit
635                    .resume_record()
636                    .map_err(RemoteParticipantError::ResumeEncode)?;
637                if let Err(error) = state.store.persist(&record.encode_canonical()) {
638                    return Err(state.brick(error));
639                }
640                let (aggregate, operation) = commit.into_parts();
641                state.aggregate = Some(aggregate);
642                Ok(RemoteOperationRecordOutcome::Recorded(
643                    RemoteParticipantOperation {
644                        operation,
645                        durability: OperationDurability::WriteAhead,
646                    },
647                ))
648            }
649            ClientOperationRecordDecision::Continuous(continuous) => {
650                let (aggregate, operation) = continuous.into_parts();
651                state.aggregate = Some(aggregate);
652                Ok(RemoteOperationRecordOutcome::Continuous(
653                    RemoteParticipantOperation {
654                        operation,
655                        durability: OperationDurability::Continuous,
656                    },
657                ))
658            }
659            ClientOperationRecordDecision::Refused(refusal) => {
660                let reason = refusal.reason();
661                let (aggregate, request) = refusal.into_parts();
662                state.aggregate = Some(aggregate);
663                Ok(RemoteOperationRecordOutcome::Refused { request, reason })
664            }
665        }
666    }
667
668    /// Declares the complete participant frame limit this handle's requests
669    /// must fit inside.
670    ///
671    /// The broker never announces its participant frame limit (the connect
672    /// acknowledgement carries only a capability bitfield), so the caller that
673    /// configured the broker declares the same number here. From then on
674    /// [`record_operation`](Self::record_operation) refuses a request whose
675    /// complete frame would exceed it, typed and before any authority is spent,
676    /// instead of letting the broker's transport gate reject the frame after the
677    /// write-ahead slot is already taken. `None` withdraws the declaration.
678    ///
679    /// # Errors
680    ///
681    /// Returns [`RemoteParticipantError::InvalidFrameLimit`] when the value is
682    /// outside the protocol's bounds for a participant frame limit.
683    pub fn declare_frame_limit(
684        &self,
685        max_frame_bytes: Option<u64>,
686    ) -> Result<(), RemoteParticipantError> {
687        let limit = match max_frame_bytes {
688            Some(value) => Some(ValidatedFrameLimit::new(value).map_err(|_| {
689                RemoteParticipantError::InvalidFrameLimit {
690                    max_frame_bytes: value,
691                }
692            })?),
693            None => None,
694        };
695        self.state.lock().frame_limit = limit;
696        Ok(())
697    }
698
699    /// The declared complete participant frame limit, if any.
700    #[must_use]
701    pub fn frame_limit(&self) -> Option<u64> {
702        self.state.lock().frame_limit.map(ValidatedFrameLimit::get)
703    }
704
705    /// Persists issued state, writes the exact operation, and retains correlation.
706    ///
707    /// # Errors
708    ///
709    /// Returns typed state, LPCR, or storage failures. Transport failures are
710    /// returned as a typed outcome after crate fate delegation.
711    pub fn send_operation(
712        &self,
713        operation: RemoteParticipantOperation,
714    ) -> Result<RemoteParticipantSendOutcome, RemoteParticipantError> {
715        let mut state = self.state.lock();
716        let mut aggregate = take_aggregate(&mut state)?;
717        if operation.durability == OperationDurability::WriteAhead {
718            aggregate = persist_retaining(&mut state, aggregate)?;
719        }
720        let (request, correlation) = operation.operation.into_request();
721        match self
722            .transport
723            .send_participant(&self.server_address, &request)
724        {
725            Ok(provenance) => {
726                if operation.durability == OperationDurability::WriteAhead {
727                    state.correlation = Some(correlation);
728                }
729                state.aggregate = Some(aggregate);
730                Ok(RemoteParticipantSendOutcome::Sent { provenance })
731            }
732            Err(error) => {
733                let operation_fate = if operation.durability == OperationDurability::WriteAhead {
734                    record_operation_transport_fate(&mut state, aggregate, correlation)
735                } else {
736                    state.aggregate = Some(aggregate);
737                    RemoteOperationTransportFate::NotOutstanding
738                };
739                let reconnect = record_connection_fate(&mut state)?;
740                Ok(RemoteParticipantSendOutcome::TransportLost {
741                    error,
742                    operation_fate,
743                    reconnect,
744                })
745            }
746        }
747    }
748
749    /// Receives one real participant frame and delegates every `ServerValue` to the crate.
750    ///
751    /// # The contract, and why it is this one
752    ///
753    /// THIS IS THE REPLY-OWED DOOR. It blocks for up to the transport's full
754    /// response deadline (60 s), because the caller it is written for has just
755    /// sent a request and is waiting for the correlated answer — and there, a
756    /// quiet connection means a slow server, not a dead one. Ending that wait
757    /// early is the 2026-08-10 outage's client-side mechanism, so this method
758    /// keeps the deadline unchanged and unconditionally.
759    ///
760    /// A consumer PUMPING an idle connection — looping to collect whatever the
761    /// server pushes next, where silence is a normal state rather than a fault
762    /// — must use [`receive_within`](Self::receive_within) or
763    /// [`try_receive`](Self::try_receive) instead. That is not a preference: a
764    /// drain loop built on this method waits out the full deadline on every
765    /// quiet read, which is how a 30 s boot gate blows on a healthy server.
766    ///
767    /// The split is by CALLER INTENT and cannot be anything else. At a clean
768    /// frame boundary with an empty buffer, a pump read and an outage-shaped
769    /// reply-owed read are byte-for-byte identical states; only the caller
770    /// knows which one it is making, so only the caller's choice of method can
771    /// carry it. Inferring it from buffered bytes, or from whether an operation
772    /// is outstanding, would silently shorten the deadline for some class of
773    /// genuinely reply-owed read and re-open the outage for it.
774    ///
775    /// Pushed deliveries are at-least-once: the same
776    /// `(conversation_id, delivery_seq)` may arrive more than once on one
777    /// healthy connection, byte-identical each time — deduplicate on the pair
778    /// (participant contract R-C3, amendment A3).
779    ///
780    /// # Errors
781    ///
782    /// Returns transport, direction, LPCR encoding, or storage failures.
783    pub fn receive(&self) -> Result<RemoteParticipantInbound, RemoteParticipantError> {
784        let frame = self
785            .transport
786            .receive_participant(&self.server_address)
787            .map_err(RemoteParticipantError::Transport)?;
788        self.classify_inbound(frame)
789    }
790
791    /// Receives one participant frame if one arrives within `budget`, reporting
792    /// a quiet connection as `Ok(None)` instead of an error.
793    ///
794    /// THE PUMP DOOR — the lawful read for a consumer that is owed nothing.
795    /// `Ok(None)` means "no frame within this window", which is a normal state
796    /// on a healthy connection and never a fault; a real transport failure
797    /// still returns `Err`, and a quiet window never surfaces a raw errno.
798    ///
799    /// `budget` is the CALLER'S bound and is spent across as many transport
800    /// read windows as it takes. It never shortens anything else: a caller that
801    /// uses this method to await a correlated answer simply names its own
802    /// deadline, and passing one at or above the transport's 60 s response
803    /// deadline reproduces [`receive`](Self::receive)'s patience with a typed
804    /// silence at the end instead of an error.
805    ///
806    /// Unlike [`receive`](Self::receive), this wait can be ended early. When
807    /// the transport offers a read wake (`RemoteConfig::read_wake`), a wake
808    /// from any handle sharing the transport ends a wait of any budget, a
809    /// correlated-answer wait included, with
810    /// `Err(RemoteParticipantError::Transport(SdkError::ReadInterrupted))`. A
811    /// caller that must not be cut short uses [`receive`](Self::receive), or
812    /// treats the interrupt as "call again".
813    ///
814    /// `Duration::ZERO` polls only what has already decoded, without arming a
815    /// read. Bytes of a partly-arrived frame stay buffered across an
816    /// `Ok(None)`, so a frame that was mid-flight when the budget expired is
817    /// never lost — the next call resumes on it.
818    ///
819    /// # Errors
820    ///
821    /// Returns transport, direction, LPCR encoding, or storage failures. A
822    /// quiet window is NOT one of them.
823    pub fn receive_within(
824        &self,
825        budget: Duration,
826    ) -> Result<Option<RemoteParticipantInbound>, RemoteParticipantError> {
827        let Some(frame) = self
828            .transport
829            .receive_participant_within(&self.server_address, budget)
830            .map_err(RemoteParticipantError::Transport)?
831        else {
832            return Ok(None);
833        };
834        self.classify_inbound(frame).map(Some)
835    }
836
837    /// What this handle's quiet drains have done on its current connection:
838    /// pumps run and reads that ended because their window closed. Reading it
839    /// touches no socket and takes no participant authority.
840    #[must_use]
841    pub fn pump_diagnostics(&self) -> ParticipantPumpDiagnostics {
842        self.transport.participant_pump_diagnostics()
843    }
844
845    /// Receives available participant traffic or observes that the broker is
846    /// quiet at an on-demand transport fence, without a fixed quiet wait.
847    ///
848    /// An available delivery or marker wake returns immediately, even if the
849    /// probe's reply has not arrived. Otherwise one `Ping` is sent and `Ok(None)`
850    /// means its `Pong` arrived with no participant frame ahead of it. The fence
851    /// proves transport progress only: never settlement, application receipt,
852    /// or the absence of traffic that arrives later.
853    ///
854    /// Use this to drain a seat, not as an idle polling loop. To wait for future
855    /// traffic, use [`receive_within`](Self::receive_within), whose explicit
856    /// budget is unchanged. [`receive`](Self::receive) also retains its full
857    /// reply-owed deadline.
858    ///
859    /// While it waits for the `Pong`, a read wake from any handle sharing the
860    /// transport ends the wait with `SdkError::ReadInterrupted`; the `Pong`
861    /// is consumed by a later read.
862    ///
863    /// # Errors
864    ///
865    /// Returns transport or decoding failures. An unanswered probe is a named
866    /// transport failure after [`PARTICIPANT_PUMP_WINDOW`], never quiet `None`.
867    pub fn try_receive(&self) -> Result<Option<RemoteParticipantInbound>, RemoteParticipantError> {
868        let frame = self
869            .transport
870            .try_receive_participant()
871            .map_err(RemoteParticipantError::Transport)?;
872        frame.map(|frame| self.classify_inbound(frame)).transpose()
873    }
874
875    /// Routes one decoded transport frame into the crate's inbound decisions.
876    ///
877    /// Shared by [`receive`](Self::receive) and
878    /// [`receive_within`](Self::receive_within) so the two doors differ ONLY in
879    /// how long they wait for a frame. Every correlation, application, and
880    /// refusal rule below is reached identically by both — a pump read that
881    /// does find a frame applies it exactly as a reply-owed read would.
882    fn classify_inbound(
883        &self,
884        frame: ParticipantTransportFrame,
885    ) -> Result<RemoteParticipantInbound, RemoteParticipantError> {
886        let ParticipantTransportFrame { frame, provenance } = frame;
887        match frame {
888            ParticipantFrame::ServerPush(value) => {
889                Ok(RemoteParticipantInbound::Push { value, provenance })
890            }
891            ParticipantFrame::ClientRequest(_) => {
892                Err(RemoteParticipantError::InvalidInboundDirection)
893            }
894            ParticipantFrame::ServerValue(value) => self.apply_inbound(value, provenance),
895        }
896    }
897
898    fn apply_inbound(
899        &self,
900        value: ServerValue,
901        provenance: ParticipantResponseProvenance,
902    ) -> Result<RemoteParticipantInbound, RemoteParticipantError> {
903        let mut state = self.state.lock();
904        let aggregate = take_aggregate(&mut state)?;
905        if let Some(correlation) = state.correlation.take() {
906            match decide_correlated_inbound(aggregate, value, correlation) {
907                ClientCorrelatedInboundDecision::Applied(applied) => {
908                    let (aggregate, value) = applied.into_parts();
909                    let aggregate = persist_retaining(&mut state, aggregate)?;
910                    state.aggregate = Some(aggregate);
911                    Ok(RemoteParticipantInbound::Applied { value, provenance })
912                }
913                ClientCorrelatedInboundDecision::AppliedRetaining(retained) => {
914                    let (aggregate, value, correlation) = retained.into_parts();
915                    if matches!(value, ServerValue::WriteOutcomeUnknown(_)) {
916                        let operation_fate =
917                            end_unknown_outcome(&mut state, aggregate, correlation);
918                        let aggregate = take_aggregate(&mut state)?;
919                        let aggregate = persist_retaining(&mut state, aggregate)?;
920                        state.aggregate = Some(aggregate);
921                        return Ok(RemoteParticipantInbound::OutcomeUnknown {
922                            value,
923                            operation_fate,
924                            provenance,
925                        });
926                    }
927                    // The value applied without answering the outstanding
928                    // operation, so its response authority is kept until the
929                    // actual answer arrives. Seated before the persist so a
930                    // failed persist bricks the handle with the authority in
931                    // place rather than silently spent.
932                    state.correlation = Some(correlation);
933                    let aggregate = persist_retaining(&mut state, aggregate)?;
934                    state.aggregate = Some(aggregate);
935                    Ok(RemoteParticipantInbound::Applied { value, provenance })
936                }
937                ClientCorrelatedInboundDecision::Refused(refusal) => {
938                    let reason = refusal.reason();
939                    let (aggregate, value, correlation) = refusal.into_parts();
940                    state.aggregate = Some(aggregate);
941                    state.correlation = Some(correlation);
942                    Ok(RemoteParticipantInbound::Refused {
943                        value,
944                        reason,
945                        provenance,
946                    })
947                }
948            }
949        } else {
950            match decide_inbound(aggregate, value) {
951                ClientInboundDecision::Applied(applied) => {
952                    let (aggregate, value) = applied.into_parts();
953                    let aggregate = persist_retaining(&mut state, aggregate)?;
954                    state.aggregate = Some(aggregate);
955                    Ok(RemoteParticipantInbound::Applied { value, provenance })
956                }
957                ClientInboundDecision::Refused(refusal) => {
958                    let reason = refusal.reason();
959                    let (aggregate, value) = refusal.into_parts();
960                    state.aggregate = Some(aggregate);
961                    Ok(RemoteParticipantInbound::Refused {
962                        value,
963                        reason,
964                        provenance,
965                    })
966                }
967            }
968        }
969    }
970}
971
972/// Header plus payload bytes of the participant frame `request` encodes to:
973/// the same number the broker's transport gate measures as the declared
974/// complete frame.
975fn request_frame_bytes(request: &ClientRequest) -> Result<u64, RemoteParticipantError> {
976    let frame = ParticipantFrame::ClientRequest(request.clone());
977    let needed = encoded_len(&frame).map_err(|error| {
978        RemoteParticipantError::Transport(SdkError::Protocol {
979            description: alloc::format!("participant request frame could not be sized: {error:?}"),
980        })
981    })?;
982    u64::try_from(needed).map_err(|_| {
983        RemoteParticipantError::Transport(SdkError::Protocol {
984            description: alloc::format!(
985                "participant request frame length {needed} does not fit the wire's u64"
986            ),
987        })
988    })
989}
990
991pub(super) fn take_aggregate<S>(
992    state: &mut RemoteParticipantState<S>,
993) -> Result<ClientParticipantAggregate, RemoteParticipantError> {
994    let aggregate = state.aggregate.take();
995    aggregate.ok_or_else(|| RemoteParticipantError::StateUnavailable {
996        source: state.unavailable.clone(),
997    })
998}
999
1000pub(super) fn persist<S: ParticipantResumeStore>(
1001    store: &mut S,
1002    aggregate: &ClientParticipantAggregate,
1003) -> Result<(), RemoteParticipantError> {
1004    let record = aggregate
1005        .resume_record()
1006        .map_err(RemoteParticipantError::ResumeEncode)?;
1007    store
1008        .persist(&record.encode_canonical())
1009        .map_err(RemoteParticipantError::Storage)
1010}
1011
1012/// Persists `aggregate`, returning it to the caller on success and RE-SEATING it
1013/// in `state` when encoding refused.
1014///
1015/// The two failure modes are not alike, and the difference is durability
1016/// ambiguity:
1017///
1018/// * [`RemoteParticipantError::Storage`] means the store was asked to commit
1019///   bytes and did not say it succeeded. Whether those bytes landed is unknown,
1020///   so no further authority may be released from this aggregate. The handle
1021///   is deliberately left bricked and the next call reports
1022///   [`StateUnavailable`](RemoteParticipantError::StateUnavailable) -- the
1023///   contract documented on that variant.
1024/// * [`RemoteParticipantError::ResumeEncode`] means `resume_record`, a pure
1025///   function of the aggregate, refused. Nothing was written and no authority
1026///   was released, so there is nothing ambiguous to protect. Dropping the
1027///   aggregate here converts a typed, catchable refusal into a permanently dead
1028///   participant.
1029///
1030/// The re-seat lives in this function rather than at each seam on purpose: the
1031/// SDK has ten `take_aggregate` -> persist -> re-seat sites, the `?` skips the
1032/// re-seat at every one of them, and a new seam would inherit the same bug.
1033/// Here it cannot be forgotten.
1034///
1035/// A caller that still needs the aggregate takes it back from the `Ok`; a
1036/// caller that does not re-seats it itself. On the `ResumeEncode` path the
1037/// aggregate is already seated, so callers must not seat it again.
1038pub(super) fn persist_retaining<S: ParticipantResumeStore>(
1039    state: &mut RemoteParticipantState<S>,
1040    aggregate: ClientParticipantAggregate,
1041) -> Result<ClientParticipantAggregate, RemoteParticipantError> {
1042    match aggregate.resume_record() {
1043        Ok(record) => match state.store.persist(&record.encode_canonical()) {
1044            Ok(()) => Ok(aggregate),
1045            // The aggregate is deliberately NOT re-seated: whether the bytes
1046            // landed is unknown, so no further authority may be released. The
1047            // cause is retained on the way past, because it is about to be the
1048            // only thing that could ever explain the `StateUnavailable` every
1049            // later call will report.
1050            Err(error) => Err(state.brick(error)),
1051        },
1052        Err(error) => {
1053            state.aggregate = Some(aggregate);
1054            Err(RemoteParticipantError::ResumeEncode(error))
1055        }
1056    }
1057}
1058
1059/// Ends the expectation a `WriteOutcomeUnknown` answered. The server could not
1060/// settle the request's write, so its answer is lost, not refused: the
1061/// expectation ends as a lost response, exactly as on a dropped transport, on a
1062/// connection that stays open, and the carrier re-presents the request with
1063/// its token. Whatever the crate decides, the correlation is retired: the
1064/// server has answered it, and no later frame will.
1065fn end_unknown_outcome<S: ParticipantResumeStore>(
1066    state: &mut RemoteParticipantState<S>,
1067    aggregate: ClientParticipantAggregate,
1068    correlation: ClientResponseCorrelation,
1069) -> RemoteOperationTransportFate {
1070    let fate = record_operation_transport_fate(state, aggregate, correlation);
1071    state.correlation = None;
1072    fate
1073}
1074
1075fn record_operation_transport_fate<S: ParticipantResumeStore>(
1076    state: &mut RemoteParticipantState<S>,
1077    aggregate: ClientParticipantAggregate,
1078    correlation: ClientResponseCorrelation,
1079) -> RemoteOperationTransportFate {
1080    match record_expected_operation_fate(
1081        aggregate,
1082        correlation,
1083        ExpectedOperationTransportFate::ResponseUnavailable,
1084    ) {
1085        liminal_protocol::client::ExpectedOperationFateDecision::Recorded {
1086            aggregate,
1087            request,
1088            ..
1089        } => {
1090            state.aggregate = Some(aggregate);
1091            RemoteOperationTransportFate::Recorded { request }
1092        }
1093        liminal_protocol::client::ExpectedOperationFateDecision::Refused {
1094            aggregate,
1095            correlation,
1096            reason: ExpectedOperationFateRefusalReason::DetachUsesReplayFate,
1097            ..
1098        } => match liminal_protocol::client::transport_fate(
1099            aggregate,
1100            correlation,
1101            liminal_protocol::client::DetachTransportFate::ResponseUnavailable,
1102        ) {
1103            liminal_protocol::client::DetachTransportFateDecision::Parked(applied) => {
1104                state.aggregate = Some(applied.into_aggregate());
1105                RemoteOperationTransportFate::DetachParked
1106            }
1107            liminal_protocol::client::DetachTransportFateDecision::Refused(refusal) => {
1108                let (aggregate, (correlation, _)) = refusal.into_parts();
1109                state.aggregate = Some(aggregate);
1110                state.correlation = Some(correlation);
1111                RemoteOperationTransportFate::Refused {
1112                    reason: ExpectedOperationFateRefusalReason::DetachUsesReplayFate,
1113                }
1114            }
1115        },
1116        liminal_protocol::client::ExpectedOperationFateDecision::Refused {
1117            aggregate,
1118            correlation,
1119            reason,
1120            ..
1121        } => {
1122            state.aggregate = Some(aggregate);
1123            state.correlation = Some(correlation);
1124            RemoteOperationTransportFate::Refused { reason }
1125        }
1126    }
1127}
1128
1129pub(super) fn record_connection_fate<S: ParticipantResumeStore>(
1130    state: &mut RemoteParticipantState<S>,
1131) -> Result<RemoteReconnectPermitOutcome, RemoteParticipantError> {
1132    let aggregate = take_aggregate(state)?;
1133    let (aggregate, outcome) = match record_transport_fate(
1134        aggregate,
1135        liminal_protocol::client::EstablishedConnectionTransportFate::Lost,
1136    ) {
1137        ReconnectPermitDecision::Permitted {
1138            aggregate,
1139            permit,
1140            result,
1141        } => (
1142            aggregate,
1143            RemoteReconnectPermitOutcome::Permitted {
1144                permit: RemoteReconnectPermit { permit },
1145                result,
1146            },
1147        ),
1148        ReconnectPermitDecision::Refused(refusal) => {
1149            let reason = refusal.reason();
1150            let result = refusal.result();
1151            let (aggregate, _) = refusal.into_parts();
1152            (
1153                aggregate,
1154                RemoteReconnectPermitOutcome::Refused { reason, result },
1155            )
1156        }
1157    };
1158    let aggregate = persist_retaining(state, aggregate)?;
1159    state.aggregate = Some(aggregate);
1160    Ok(outcome)
1161}
1162
1163#[cfg(test)]
1164mod tests;