Skip to main content

offline_protocol/protocol/
send.rs

1//! Send pipeline, outbox management, and delivery tracking.
2
3use super::custody::CUSTODY_META_KEY;
4use super::{
5    base64_encode, internal_prefixes, lifetime_expired, lock_shared_state, CoalescedAcks,
6    ConnectionAcceptedPayload, ConnectionRequestPayload, KeyPackagePayload, MediaSendOptions,
7    MediaTransferDescriptor, OfflineProtocol, OutboundMediaTransfer, OutboundSendPreparation,
8    OutboxEntry, OutboxReseal, PendingConnectionRequest, PendingMessage, PendingProvenance,
9    PresencePayload, ProtocolState, ReadReceiptPayload, RichPayloadV1, RichSendExtras,
10    SendMessageOptions, TypingIndicatorPayload, WelcomeDeliveryState, MAX_INITIAL_MESSAGE_BYTES,
11    MAX_KEY_PACKAGE_SENT_TO, MAX_MESSAGE_CONTENT_BYTES, MAX_PENDING_CONNECTION_REQUESTS,
12    MAX_PENDING_EXPIRIES_PER_PASS, MAX_PENDING_MESSAGES_GLOBAL, MAX_PENDING_MESSAGES_PER_PEER,
13    MAX_PENDING_MESSAGE_BYTES_GLOBAL, MAX_PENDING_MESSAGE_BYTES_PER_PEER, MAX_READ_RECEIPT_IDS,
14    MAX_RICH_EXTRAS_BYTES, MLS_ENVELOPE_COMPACT_V1, PEER_SEND_WINDOW,
15    PENDING_CONNECTION_REQUEST_TTL, RESEAL_GAP_BUDGET, RESEAL_HARD_LIMIT, RICH_PAYLOAD_V1,
16    SEND_DEFER_REASON_ACK_CAPACITY, SEND_DEFER_REASON_PEER_WINDOW,
17    SEND_FAIL_REASON_RECIPIENT_UNREACHABLE, SEND_FAIL_REASON_RELAY_PUSHED,
18    SEND_FAIL_REASON_RELAY_PUSHED_STORED, SEND_FAIL_REASON_RELAY_STORED,
19    SEND_FAIL_REASON_TRANSPORT, WELCOME_NO_CARRIER_RETRY_SECS, WELCOME_UNREACHABLE_RETRY_CAP_SECS,
20};
21use super::{classify_transport_send_error, send_failure_token};
22use crate::constants::{
23    ACK_COALESCE_MAX, ACK_COALESCE_WINDOW, ACK_FOR_KEY, ACK_HOP_COUNT_KEY, ACK_MORE_KEY,
24    ACK_STATUS_KEY, ACK_STATUS_UNDECRYPTABLE, ACK_TRANSPORT_KEY, MAX_FORWARD_COUNT,
25    MAX_OUTBOX_ENTRIES,
26};
27use crate::events::{DecryptionFailureCode, Event, PresenceStatus};
28use crate::file_transfer::{FileChunk, OutboundTransferState};
29use crate::media_envelope::{encode_media_envelope, MediaChunkPlaintext, MediaRichExtras};
30use crate::mls_observability::{DecryptionFailureKind, MlsErrorCategory, MlsOperationContext};
31use crate::protocol::reachability::{Claim, FactSource};
32use crate::transport_manager::TransportManager;
33use crate::{Error, Result};
34use chrono::{DateTime, Utc};
35use offline_protocol_core::{
36    AppId, ContentType, ForwardInfo, MediaMetadata, Message, MessageId, MessagePriority, UserId,
37    TTL,
38};
39use offline_protocol_mls::{EncryptedMessage, MlsManager};
40use offline_protocol_transport::TransportType;
41use std::collections::{HashMap, HashSet};
42use std::sync::{Arc, RwLock};
43use std::time::{Duration as StdDuration, Instant};
44use tracing::{debug, error, info, warn};
45
46/// Most delivery acknowledgements held for want of a route (see
47/// `route_ack`). ~200 bytes each, so a ceiling of ~100 KB.
48pub(crate) const MAX_UNROUTED_ACKS: usize = 500;
49
50/// How long a held acknowledgement stays worth sending. Short on purpose:
51/// it bridges a carrier coming up moments later, and a sender still waiting
52/// past it re-sends, which the duplicate re-ACK path answers.
53pub(crate) const UNROUTED_ACK_TTL: StdDuration = StdDuration::from_secs(600);
54
55impl OfflineProtocol {
56    // ========================================================================
57    // CORE SEND
58    // ========================================================================
59
60    /// Sends a message.
61    ///
62    /// # Arguments
63    ///
64    /// * `recipient` - Recipient's user ID
65    /// * `content` - Message content
66    /// * `priority` - Message priority (optional, defaults to Medium)
67    /// * `reply_to_msg` - ID of the message this is replying to (optional)
68    ///
69    /// # Returns
70    ///
71    /// Returns the message ID if successful.
72    ///
73    /// # Auto-Encryption
74    ///
75    /// When encryption is enabled and MLS is initialized, messages are automatically
76    /// encrypted before sending. If no session exists with the recipient but we have
77    /// their key package, a session is created automatically. If no key package is
78    /// available and `store_pending` is enabled, the message is queued until a key
79    /// package is received.
80    pub fn send_message(
81        &mut self,
82        recipient: impl Into<String>,
83        content: impl Into<String>,
84        priority: Option<MessagePriority>,
85        reply_to_msg: Option<impl Into<String>>,
86    ) -> Result<MessageId> {
87        self.send_message_with(
88            recipient,
89            content,
90            SendMessageOptions {
91                priority,
92                reply_to_msg: reply_to_msg.map(Into::into),
93                ..Default::default()
94            },
95        )
96    }
97
98    /// Sends a message with rich options: priority and reply threading (as
99    /// on [`Self::send_message`]), plus quoted-reply context, rich media
100    /// metadata, and forward attribution.
101    ///
102    /// The rich fields only ever travel inside the MLS-sealed `__RICH_V1__`
103    /// body, and only toward recipients whose key package advertised
104    /// `rich_versions` support (gated by
105    /// `EncryptionConfig::rich_payload_enabled`). Toward anyone else they
106    /// are silently dropped — never sent cleartext — so the message degrades
107    /// to plain text with `reply_to_msg` threading intact.
108    ///
109    /// Returns `InvalidArgument` for `ContentType::FileChunk` (an internal
110    /// transport content type — the receiver would swallow the message into
111    /// its file-transfer manager) and for rich extras whose serialized size
112    /// exceeds 32 KiB (oversized bodies inflate the MLS plaintext into heavy
113    /// transport fragmentation).
114    pub fn send_message_with(
115        &mut self,
116        recipient: impl Into<String>,
117        content: impl Into<String>,
118        options: SendMessageOptions,
119    ) -> Result<MessageId> {
120        // Check if protocol is running
121        {
122            let state = lock_shared_state(&self.shared_state)?;
123            if state.state != ProtocolState::Running {
124                return Err(Error::NotStarted);
125            }
126        }
127
128        let recipient_str: String = recipient.into();
129        let content_str: String = content.into();
130        let priority = options.priority.unwrap_or(MessagePriority::Medium);
131        Self::validate_outbound_recipient(&recipient_str)?;
132        // Parsed first, before anything can queue the send or tick the
133        // Lamport clock: a bad id must leave no durable trace.
134        let app_id = Self::parse_send_app_id(options.app_id.as_deref())?;
135
136        // Prevent sending messages to blocked users. Blocking is bidirectional:
137        // we neither receive from nor send to a blocked peer.
138        if self.is_user_blocked(&recipient_str) {
139            return Err(Error::UserBlocked(recipient_str));
140        }
141
142        Self::check_content_size(&content_str)?;
143
144        // Reject content that starts with an internal control prefix to prevent
145        // injection of protocol-level messages through the public API.
146        if Self::is_internal_prefix(&content_str) {
147            return Err(Error::InvalidArgument(
148                "Message content must not start with a reserved internal prefix".to_string(),
149            ));
150        }
151
152        // FileChunk is an internal transport content type: the receiver
153        // routes a message stamped with it into handle_incoming_file_chunk
154        // (after ACKing delivery) where a non-chunk fails to parse and is
155        // dropped without surfacing. Reject rather than silently lose it.
156        if options.content_type == Some(ContentType::FileChunk) {
157            return Err(Error::InvalidArgument(
158                "FileChunk is an internal content type and cannot be sent directly".to_string(),
159            ));
160        }
161
162        // Parse reply_to_msg if provided
163        let reply_to_msg_id = options
164            .reply_to_msg
165            .as_deref()
166            .map(MessageId::from_str)
167            .transpose()
168            .map_err(|e| Error::InvalidArgument(format!("Invalid reply_to_msg: {}", e)))?;
169
170        let rich = RichSendExtras {
171            reply_context: options.reply_context,
172            media_metadata: options.media_metadata,
173            forward_info: options.forward_info,
174        };
175
176        // Bound the extras here at the boundary — not at seal time — so the
177        // pending queue only ever holds extras that are known to seal: the
178        // flush path re-seals against current capability, and a seal-time
179        // failure there would re-queue the message forever. The cap keeps an
180        // oversized quote or thumbnail from inflating the MLS plaintext into
181        // heavy transport fragmentation.
182        rich.check_size()?;
183
184        let final_content = match self.prepare_outbound_content(
185            &recipient_str,
186            &content_str,
187            priority,
188            reply_to_msg_id.clone(),
189            None,
190            options.content_type.unwrap_or_default(),
191            None,
192            Some(&rich),
193            app_id.as_ref(),
194            None,
195            "send_message_session_pending",
196        )? {
197            OutboundSendPreparation::Ready(content) => content,
198            OutboundSendPreparation::Queued(message_id) => return Ok(message_id),
199        };
200
201        // Create message with potentially encrypted content. Rich extras are
202        // deliberately NOT copied onto the outer message — they are either
203        // inside the sealed body by now or dropped; only the coarse
204        // content_type rendering hint rides outer.
205        let reseal_reply_to = reply_to_msg_id.clone();
206        let mut message = self.create_message_for_app(
207            &recipient_str,
208            final_content,
209            Some(priority),
210            reply_to_msg_id,
211            app_id,
212        )?;
213        if let Some(content_type) = options.content_type {
214            message.content_type = content_type;
215        }
216
217        // Tier 2: if the send was encrypted, keep the plaintext provenance so
218        // each resend re-seals against the peer's current session instead of
219        // replaying dead-epoch ciphertext (see `OutboxReseal`).
220        if message.content.starts_with(internal_prefixes::ENCRYPTED) {
221            self.stage_outbox_reseal(
222                &message.id,
223                &recipient_str,
224                OutboxReseal {
225                    content: content_str,
226                    priority,
227                    reply_to_msg: reseal_reply_to,
228                    forwarded_from: None,
229                    content_type: message.content_type,
230                    media_metadata: None,
231                    rich: Some(rich).filter(|r| r.is_any()),
232                    seal_mark: None,
233                },
234            );
235        }
236
237        match options.via_transport {
238            Some(transport) => self.dispatch_prepared_message_via(message, transport),
239            None => self.dispatch_prepared_message(message),
240        }
241    }
242
243    /// Forwards a message to a new recipient with attribution to the original sender.
244    ///
245    /// Creates a new message with the original content and attaches `ForwardInfo`
246    /// tracking the original sender, message ID, timestamp, and forward count.
247    /// If the message was already forwarded, the original attribution is preserved
248    /// and `forward_count` is incremented.
249    ///
250    /// Toward a recipient that advertised the sealed rich payload, the
251    /// attribution and the original `media_metadata` — including any
252    /// cloud-media `encryption_key`/`iv` secrets — travel inside the MLS-sealed
253    /// `__RICH_V1__` body, so forwarded cloud media stays openable. Toward
254    /// anyone else they ride the legacy cleartext outer fields, from which
255    /// the transport chokepoint strips the secrets — attribution and display
256    /// hints survive, but the media key cannot be delivered.
257    ///
258    /// # Arguments
259    ///
260    /// * `original_message` - The message to forward
261    /// * `new_recipient` - Recipient's user ID
262    /// * `priority` - Message priority (optional, defaults to Medium)
263    ///
264    /// # Returns
265    ///
266    /// Returns the new message ID if successful.
267    pub fn forward_message(
268        &mut self,
269        original_message: &Message,
270        new_recipient: impl Into<String>,
271        priority: Option<MessagePriority>,
272    ) -> Result<MessageId> {
273        {
274            let state = lock_shared_state(&self.shared_state)?;
275            if state.state != ProtocolState::Running {
276                return Err(Error::NotStarted);
277            }
278        }
279
280        let recipient_str: String = new_recipient.into();
281        let priority = priority.unwrap_or(MessagePriority::Medium);
282        Self::validate_outbound_recipient(&recipient_str)?;
283
284        if self.is_user_blocked(&recipient_str) {
285            return Err(Error::UserBlocked(recipient_str));
286        }
287
288        // Reject content that starts with an internal control prefix to prevent
289        // injection of protocol-level messages through the forwarding API.
290        if Self::is_internal_prefix(&original_message.content) {
291            return Err(Error::InvalidArgument(
292                "Cannot forward a message with reserved internal prefix content".to_string(),
293            ));
294        }
295
296        // FileChunk is an internal transport content type (see
297        // `send_message_with`): a forwarded message stamped with it would be
298        // ACKed and then dropped by the receiver's file-transfer manager.
299        if original_message.content_type == ContentType::FileChunk {
300            return Err(Error::InvalidArgument(
301                "FileChunk is an internal content type and cannot be forwarded".to_string(),
302            ));
303        }
304
305        // Build ForwardInfo: preserve original attribution, increment count
306        let forward_info = ForwardInfo::from_message(original_message);
307
308        if forward_info.forward_count > MAX_FORWARD_COUNT {
309            return Err(Error::InvalidArgument(format!(
310                "Forward count {} exceeds maximum of {}",
311                forward_info.forward_count, MAX_FORWARD_COUNT,
312            )));
313        }
314
315        // Sealed-path extras: toward a rich-capable recipient the attribution
316        // and the original media metadata (including cloud-media key/iv
317        // secrets, which only the sealed body may carry — the wire chokepoint
318        // strips them from the cleartext outer field) travel inside the
319        // `__RICH_V1__` body. The outer copies set below remain the fallback
320        // for non-capable recipients; the receiver's sealed-body restore is
321        // authoritative and overwrites them when the seal happened.
322        let rich = RichSendExtras {
323            reply_context: None,
324            media_metadata: original_message.media_metadata.clone(),
325            forward_info: Some(forward_info.clone()),
326        };
327        // Same boundary caps as `send_message_with` — enforced here, not at
328        // seal time, so a forward queued behind session establishment is
329        // always known to seal at flush and always fits the queue's budget.
330        Self::check_content_size(&original_message.content)?;
331        rich.check_size()?;
332
333        // Prepare content (may encrypt). ForwardInfo is threaded through so
334        // it survives the pending-message queue if the session isn't ready.
335        let final_content = match self.prepare_outbound_content(
336            &recipient_str,
337            &original_message.content,
338            priority,
339            None,
340            Some(forward_info.clone()),
341            original_message.content_type,
342            original_message.media_metadata.clone(),
343            Some(&rich),
344            None,
345            None,
346            "forward_message_session_pending",
347        )? {
348            OutboundSendPreparation::Ready(content) => content,
349            OutboundSendPreparation::Queued(message_id) => return Ok(message_id),
350        };
351
352        // Create and dispatch the forwarded message
353        let mut message =
354            self.create_message_for_app(&recipient_str, final_content, Some(priority), None, None)?;
355        message.forwarded_from = Some(forward_info);
356        message.content_type = original_message.content_type;
357        message.media_metadata = original_message.media_metadata.clone();
358
359        // Tier 2 provenance (see `send_message_with`): re-seal forwarded encrypted
360        // DMs against the peer's current session on resend.
361        if message.content.starts_with(internal_prefixes::ENCRYPTED) {
362            self.stage_outbox_reseal(
363                &message.id,
364                &recipient_str,
365                OutboxReseal {
366                    content: original_message.content.clone(),
367                    priority,
368                    reply_to_msg: None,
369                    forwarded_from: message.forwarded_from.clone(),
370                    content_type: message.content_type,
371                    media_metadata: message.media_metadata.clone(),
372                    rich: Some(rich).filter(|r| r.is_any()),
373                    seal_mark: None,
374                },
375            );
376        }
377
378        self.dispatch_prepared_message(message)
379    }
380
381    /// Shared send path: dedup, transport send, success/failure handling, event emission.
382    ///
383    /// Used by `forward_message`, `flush_pending_messages` (for forwarded messages),
384    /// and any path that has a fully-constructed `Message` ready to send.
385    fn dispatch_prepared_message(&mut self, message: Message) -> Result<MessageId> {
386        // Restored, not cleared: a dispatch nested inside this one would
387        // otherwise clear the outer message's staging on its way out, and the
388        // outer entry would be created as not the app's.
389        let outer = self.staged_app_send.replace(message.id.clone());
390        let result = self.dispatch_prepared_message_inner(message);
391        self.staged_app_send = outer;
392        result
393    }
394
395    /// Whether an outbox entry being created for `message` belongs to the app
396    /// send in progress; see `staged_app_send`. Every outcome of an app send
397    /// that keeps the message (sent, deferred, held for a window, parked for
398    /// capacity) creates its entry inside the dispatch, so the flag is on the
399    /// entry before its first write to storage.
400    fn is_staged_app_send(&self, message: &Message) -> bool {
401        self.staged_app_send.as_ref() == Some(&message.id)
402    }
403
404    fn dispatch_prepared_message_inner(&mut self, message: Message) -> Result<MessageId> {
405        let message_id = message.id.clone();
406
407        if self.deduplicator.is_duplicate(&message_id) {
408            return Err(crate::Error::Other("Duplicate message".to_string()));
409        }
410
411        self.deduplicator.mark_seen_local(message_id.clone());
412
413        if self.refuse_app_message_at_capacity(&message)
414            || self.defer_app_message_for_ack_capacity(&message)
415            || self.defer_app_message_for_peer_window(&message)
416        {
417            return Ok(message_id);
418        }
419
420        let previous_transport = self.transport_manager.current_transport();
421
422        // A recipient this device holds a live mesh link to is reached over
423        // that link, even with infrastructure up. Without this the online
424        // sender standing next to an offline recipient routes through the
425        // relay, which answers `recipient_unreachable`, and delivery waits for
426        // the park machinery to offer the frame to the very neighbour that was
427        // addressable all along. DORS cannot make this call: it scores
428        // carriers, and the fact that decides it is about the recipient.
429        //
430        // The link is ground truth, not a stored claim, so there is nothing to
431        // go stale. A refusal here is not terminal: the frame falls through to
432        // selection exactly as if this had never been tried, which keeps a
433        // marginal radio link from stranding a message another carrier could
434        // take.
435        let direct_mesh = self
436            .transport_manager
437            .mesh_transport_for(message.recipient.as_str());
438        let mut sent_via_mesh = None;
439        if let Some(transport) = direct_mesh {
440            match self
441                .transport_manager
442                .send_via_transport(&message, transport)
443            {
444                Ok(()) => sent_via_mesh = Some(transport),
445                Err(err) => {
446                    self.transport_manager.record_retry_failure(transport);
447                    debug!(
448                        message_id = %message.id,
449                        transport = ?transport,
450                        error = %err,
451                        "Direct mesh link refused the frame; falling back to transport selection"
452                    );
453                }
454            }
455        }
456
457        let (send_result, current_transport) = match sent_via_mesh {
458            Some(transport) => (Ok(()), Some(transport)),
459            None => {
460                let result = self.transport_manager.send(&message);
461                (result, self.transport_manager.current_transport())
462            }
463        };
464
465        match send_result {
466            Ok(()) => {
467                self.handle_send_success(&message, current_transport)?;
468                self.emit_transport_switch_event(previous_transport, current_transport)?;
469                self.emit_message_sent_event(&message)?;
470                Ok(message_id)
471            }
472            Err(err) => {
473                let next_retry_at =
474                    self.handle_send_failure(&message, current_transport.or(previous_transport))?;
475                warn!(
476                    message_id = %message.id,
477                    error = %err,
478                    "Send failed, message deferred"
479                );
480                self.emit_event(Event::message_deferred(
481                    message.id.clone(),
482                    message.recipient.as_str().to_string(),
483                    send_failure_token(&err),
484                    0,
485                    next_retry_at.map(|at| at.timestamp_millis()),
486                ));
487                Ok(message_id)
488            }
489        }
490    }
491
492    /// [`Self::dispatch_prepared_message`] variant that sends via a specific
493    /// transport (bypassing DORS) — the shared tail for
494    /// `send_message_via_transport` and rich sends carrying `via_transport`.
495    /// Unlike `TransportManager::send`, `send_via_transport` does not record
496    /// retry failures internally, so the failure arm records one explicitly.
497    fn dispatch_prepared_message_via(
498        &mut self,
499        message: Message,
500        transport: TransportType,
501    ) -> Result<MessageId> {
502        // Restored, not cleared; see `dispatch_prepared_message`.
503        let outer = self.staged_app_send.replace(message.id.clone());
504        let result = self.dispatch_prepared_message_via_inner(message, transport);
505        self.staged_app_send = outer;
506        result
507    }
508
509    fn dispatch_prepared_message_via_inner(
510        &mut self,
511        message: Message,
512        transport: TransportType,
513    ) -> Result<MessageId> {
514        let message_id = message.id.clone();
515
516        if self.deduplicator.is_duplicate(&message_id) {
517            return Err(crate::Error::Other("Duplicate message".to_string()));
518        }
519
520        self.deduplicator.mark_seen_local(message_id.clone());
521
522        if self.refuse_app_message_at_capacity(&message)
523            || self.defer_app_message_for_ack_capacity(&message)
524            || self.defer_app_message_for_peer_window(&message)
525        {
526            return Ok(message_id);
527        }
528
529        let previous_transport = self.transport_manager.current_transport();
530        let send_result = self
531            .transport_manager
532            .send_via_transport(&message, transport);
533        let current_transport = Some(transport);
534
535        match send_result {
536            Ok(()) => {
537                self.handle_send_success(&message, current_transport)?;
538                self.emit_transport_switch_event(previous_transport, current_transport)?;
539                self.emit_message_sent_event(&message)?;
540                Ok(message_id)
541            }
542            Err(err) => {
543                let next_retry_at =
544                    self.handle_send_failure(&message, current_transport.or(previous_transport))?;
545                // send_via_transport does not record retry failures internally
546                // (unlike TransportManager::send), so record explicitly here.
547                self.transport_manager.record_retry_failure(transport);
548                warn!(
549                    message_id = %message.id,
550                    transport = ?transport,
551                    error = %err,
552                    "Send via forced transport failed, message deferred"
553                );
554                // The forced transport is in the `warn!` above, not on the
555                // event: it is a routing detail, and the token names the
556                // failure class either way.
557                self.emit_event(Event::message_deferred(
558                    message.id.clone(),
559                    message.recipient.as_str().to_string(),
560                    send_failure_token(&err),
561                    0,
562                    next_retry_at.map(|at| at.timestamp_millis()),
563                ));
564                Ok(message_id)
565            }
566        }
567    }
568
569    /// Sends an internal protocol message (connection requests, etc.) via DORS.
570    ///
571    /// Handles the full send orchestration: state check, deduplication, transport send,
572    /// success/failure handling, and transport switch events. Does NOT emit a
573    /// `MessageSent` event — internal messages are not user-visible content.
574    pub(crate) fn send_internal_message(
575        &mut self,
576        recipient: &str,
577        content: String,
578        priority: MessagePriority,
579    ) -> Result<MessageId> {
580        self.send_internal_message_alerting(recipient, content, priority, false)
581    }
582
583    /// [`Self::send_internal_message`], with `alert` choosing whether the
584    /// relay may push for the frame. `true` only for the control frames that
585    /// carry something the user acts on or reads: a connection request or
586    /// accept, and a group chat message. Everything else is
587    /// [`crate::constants::NO_ALERT_KEY`]-marked.
588    pub(crate) fn send_internal_message_alerting(
589        &mut self,
590        recipient: &str,
591        content: String,
592        priority: MessagePriority,
593        alert: bool,
594    ) -> Result<MessageId> {
595        {
596            let state = lock_shared_state(&self.shared_state)?;
597            if state.state != ProtocolState::Running {
598                return Err(Error::NotStarted);
599            }
600        }
601
602        let mut message = if alert {
603            self.create_message_for_app(recipient, content, Some(priority), None, None)?
604        } else {
605            self.create_message(recipient, content, Some(priority), None)?
606        };
607        self.sign_control_message(&mut message)?;
608        self.dispatch_internal_message(message)
609    }
610
611    /// Sends an internal frame already sealed for `recipient` (an
612    /// `__MLS_ENC__` body), unsigned.
613    ///
614    /// Not signed because the data plane is not: a 1:1 envelope is
615    /// authenticated by MLS, and the security gate admits `__MLS_ENC__`
616    /// without a signature for exactly that reason. `reseal` carries the
617    /// plaintext so a resend after a desync re-key is sealed again against the
618    /// new epoch rather than replaying ciphertext nobody can open any more.
619    pub(super) fn send_sealed_internal_message(
620        &mut self,
621        recipient: &str,
622        sealed: String,
623        priority: MessagePriority,
624        reseal: Option<OutboxReseal>,
625    ) -> Result<MessageId> {
626        {
627            let state = lock_shared_state(&self.shared_state)?;
628            if state.state != ProtocolState::Running {
629                return Err(Error::NotStarted);
630            }
631        }
632
633        let message = self.create_message(recipient, sealed, Some(priority), None)?;
634        if let Some(reseal) = reseal {
635            self.stage_outbox_reseal(&message.id, recipient, reseal);
636        }
637        self.dispatch_internal_message(message)
638    }
639
640    /// Transmits a built internal frame: dedup, send, the success or failure
641    /// bookkeeping (outbox, ACK, retry) and the transport-switch event. Shared
642    /// by the signed and the sealed internal senders so the two cannot drift.
643    fn dispatch_internal_message(&mut self, message: Message) -> Result<MessageId> {
644        let message_id = message.id.clone();
645
646        if self.deduplicator.is_duplicate(&message_id) {
647            // Staged re-seal provenance must never outlive its message.
648            self.pending_reseal.remove(&message_id);
649            return Err(crate::Error::Other("Duplicate message".to_string()));
650        }
651
652        self.deduplicator.mark_seen_local(message_id.clone());
653
654        // Internal frames owe the app no event: the retry queue sends it.
655        if self.defer_for_ack_capacity(&message).is_some() {
656            return Ok(message_id);
657        }
658
659        let previous_transport = self.transport_manager.current_transport();
660        let send_result = self.transport_manager.send(&message);
661        let current_transport = self.transport_manager.current_transport();
662
663        match send_result {
664            Ok(()) => {
665                self.handle_send_success(&message, current_transport)?;
666            }
667            Err(err) => {
668                let _ =
669                    self.handle_send_failure(&message, current_transport.or(previous_transport))?;
670                warn!(
671                    message_id = %message.id,
672                    recipient = %message.recipient,
673                    error = %err,
674                    "Internal message send failed, message deferred"
675                );
676            }
677        }
678
679        self.emit_transport_switch_event(previous_transport, current_transport)?;
680        Ok(message_id)
681    }
682
683    /// Sends a self-addressed relay *hint* frame — `__GRP_RELAY_REG__` or
684    /// `__GRP_RELAY_BCAST__` — pinned to the Internet transport, without the
685    /// ACK ladder.
686    ///
687    /// A hint frame is not traffic. The platform bridge recognizes it via
688    /// [`Self::internet_control_op`] and **replaces** it with a relay-native
689    /// frame (`CreateGroup` + member deltas, or `SendGroupMessage`), so the
690    /// frame itself never reaches a peer and no delivery ACK can ever come
691    /// back for it. Two properties follow, and both are load-bearing:
692    ///
693    /// 1. **`requires_ack = false`.** On the ordinary ladder an un-ACKable
694    ///    frame is retransmitted `DEFAULT_MAX_RETRIES` times over ~800s, each
695    ///    resend producing another full relay fan-out (under a fresh
696    ///    relay-minted id, so receiver dedup misses it), and terminates in a
697    ///    `MessageFailed` for an id the app was never told about plus a
698    ///    `record_delivery_failure` that degrades DORS' score for the very
699    ///    transport the frame requires. Opting out of the ladder is what makes
700    ///    these frames honest one-shots; retry policy for them lives at the
701    ///    application layer instead (`RelayRegisterPending` for registration,
702    ///    the caller's per-member fallback for broadcast).
703    /// 2. **Pinned to [`TransportType::Internet`].** These frames are
704    ///    self-addressed, and DORS demotes Internet below every mesh transport
705    ///    (`INTERNET_FALLBACK_DEMOTION`), so ordinary routing hands them to
706    ///    the mesh carriers first. BLE and Wi-Fi Direct fail closed (self is
707    ///    never a connected peer), but Reticulum enqueues unconditionally and
708    ///    returns `Ok`, swallowing the frame while reporting success, which on
709    ///    the broadcast path means the group message is delivered to nobody.
710    ///
711    /// Errors propagate to the caller rather than routing through
712    /// [`Self::handle_send_failure`]: a hint frame has no outbox entry to
713    /// defer into, and its callers already have a better recovery than a
714    /// retry (fall back to per-member fan-out; leave the group unregistered
715    /// for the next sync tick). The failure is also deliberately *not*
716    /// recorded against Internet's DORS metrics — "the relay bridge isn't up"
717    /// is not evidence about the transport's delivery quality.
718    ///
719    /// No `TransportChanged` event is emitted, matching
720    /// [`Self::send_delivery_ack`] — the other fire-and-forget control frame
721    /// sent over a forced transport. A hint frame is not user traffic, and
722    /// announcing a switch for one would report a transport change the app's
723    /// messages did not take.
724    ///
725    /// `alert` is false for a registration and true for a broadcast, which
726    /// carries a group chat message (see [`Self::send_internal_message_alerting`]).
727    pub(crate) fn send_relay_hint_message(
728        &mut self,
729        content: String,
730        priority: MessagePriority,
731        alert: bool,
732    ) -> Result<MessageId> {
733        {
734            let state = lock_shared_state(&self.shared_state)?;
735            if state.state != ProtocolState::Running {
736                return Err(Error::NotStarted);
737            }
738        }
739
740        let self_id = self.local_id.clone();
741        let mut message = if alert {
742            self.create_message_for_app(&self_id, content, Some(priority), None, None)?
743        } else {
744            self.create_message(&self_id, content, Some(priority), None)?
745        };
746        // Set before signing for clarity; the canonical payload covers only
747        // sender/id/recipient/content, so ordering is not security-relevant.
748        message.requires_ack = false;
749        self.sign_control_message(&mut message)?;
750        let message_id = message.id.clone();
751
752        if self.deduplicator.is_duplicate(&message_id) {
753            return Err(crate::Error::Other("Duplicate message".to_string()));
754        }
755
756        // Mark seen so a bridge that passes the frame through verbatim (an
757        // adapter without a translator) cannot have the relay echo it back
758        // into our own receive path.
759        self.deduplicator.mark_seen_local(message_id.clone());
760
761        self.transport_manager
762            .send_via_transport(&message, TransportType::Internet)?;
763
764        Ok(message_id)
765    }
766
767    /// Sends one MLS-encrypted session-confirm marker to `peer_id`.
768    ///
769    /// Used by the Welcome-adopt path: after we adopt the peer's `session:` group
770    /// (and confirm our own side), the peer may be the both-create "owner" that
771    /// kept its own group and confirms ONLY on a group-aware decrypt from us. A
772    /// plaintext probe/ack does not count, and we may have no user traffic to
773    /// send — so without this the owner stays Pending forever and the 1:1
774    /// connection never completes. The marker is `SESSION_CONFIRM_ENCRYPTED`
775    /// wrapped in the normal `ENCRYPTED` envelope; the peer consumes it on decrypt
776    /// (never surfaced). Mirrors [`Self::send_internal_message`] but encrypts
777    /// (data-plane, so NOT signed) and never propagates errors — it is re-sent on
778    /// every received Welcome while still adopting, so a lost confirm is retried
779    /// in lockstep with the owner's Welcome retransmission.
780    pub(super) fn send_session_confirm_encrypted(&mut self, peer_id: &str) {
781        let encrypted = match self.encrypt_content_for_recipient(
782            peer_id,
783            internal_prefixes::SESSION_CONFIRM_ENCRYPTED,
784            MessagePriority::High,
785        ) {
786            Ok(content) => content,
787            Err(err) => {
788                debug!(
789                    peer = %peer_id,
790                    error = %err,
791                    "Skipped proactive session-confirm (encryption not ready); welcome retry remains"
792                );
793                return;
794            }
795        };
796
797        let message =
798            match self.create_message(peer_id, encrypted, Some(MessagePriority::High), None) {
799                Ok(message) => message,
800                Err(err) => {
801                    warn!(peer = %peer_id, error = %err, "Failed to build session-confirm message");
802                    return;
803                }
804            };
805
806        if self.deduplicator.is_duplicate(&message.id) {
807            return;
808        }
809        self.deduplicator.mark_seen_local(message.id.clone());
810
811        if self.defer_for_ack_capacity(&message).is_some() {
812            return;
813        }
814
815        let previous_transport = self.transport_manager.current_transport();
816        match self.transport_manager.send(&message) {
817            Ok(()) => {
818                let current_transport = self.transport_manager.current_transport();
819                let _ = self.handle_send_success(&message, current_transport);
820                info!(peer = %peer_id, "Sent proactive encrypted session-confirm to owner");
821            }
822            Err(err) => {
823                let current_transport = self.transport_manager.current_transport();
824                let _ =
825                    self.handle_send_failure(&message, current_transport.or(previous_transport));
826                debug!(
827                    peer = %peer_id,
828                    error = %err,
829                    "Proactive session-confirm deferred (will retry with welcome)"
830                );
831            }
832        }
833    }
834
835    /// Sends a message via a specific transport type (bypassing DORS).
836    pub fn send_message_via_transport(
837        &mut self,
838        recipient: impl Into<String>,
839        content: impl Into<String>,
840        priority: Option<MessagePriority>,
841        transport: TransportType,
842        reply_to_msg: Option<impl Into<String>>,
843    ) -> Result<MessageId> {
844        self.send_message_with(
845            recipient,
846            content,
847            SendMessageOptions {
848                priority,
849                reply_to_msg: reply_to_msg.map(Into::into),
850                via_transport: Some(transport),
851                ..Default::default()
852            },
853        )
854    }
855
856    /// Rejects a recipient token that is not a well-formed [`UserId`].
857    ///
858    /// Enforced at every outbound boundary so an app-owned placeholder (an
859    /// `unresolved:token`, an empty string) cannot become durable protocol
860    /// state — an outbox entry or pending-queue record that is retried, stored,
861    /// and restored forever against an address no transport can ever resolve.
862    pub(crate) fn validate_outbound_recipient(recipient: &str) -> Result<()> {
863        UserId::new(recipient)
864            .map(|_| ())
865            .map_err(|error| Error::InvalidArgument(format!("Invalid recipient user ID: {error}")))
866    }
867
868    /// Creates a new message stamped with the configured app id.
869    ///
870    /// Every engine-originated frame goes through here: control traffic,
871    /// key packages, Welcomes, data-sync frames and forwards. Those belong
872    /// to the engine, not to an application, so they always carry
873    /// [`crate::ProtocolConfig::app_id`]. Only an application's own send
874    /// can name another id, through [`Self::create_message_for_app`].
875    ///
876    /// For the same reason every frame built here carries
877    /// [`crate::constants::NO_ALERT_KEY`]: nothing the user wrote is in it,
878    /// so the relay must not push "new message" for it. User content is
879    /// built through [`Self::create_message_for_app`], which does not mark.
880    pub(super) fn create_message(
881        &mut self,
882        recipient: impl Into<String>,
883        content: impl Into<String>,
884        priority: Option<MessagePriority>,
885        reply_to_msg: Option<MessageId>,
886    ) -> Result<Message> {
887        let mut message =
888            self.create_message_for_app(recipient, content, priority, reply_to_msg, None)?;
889        message
890            .metadata
891            .insert(crate::constants::NO_ALERT_KEY.to_string(), "1".to_string());
892        Ok(message)
893    }
894
895    /// Validates a per-send app id from the rich send surface.
896    ///
897    /// Same rules as the configured id and the wire decoder (`AppId::new`),
898    /// reported as `InvalidArgument` because it is a call argument, not
899    /// configuration.
900    pub(super) fn parse_send_app_id(app_id: Option<&str>) -> Result<Option<AppId>> {
901        app_id
902            .map(AppId::new)
903            .transpose()
904            .map_err(|e| Error::InvalidArgument(format!("Invalid app_id: {e}")))
905    }
906
907    /// Like [`Self::create_message`], stamping `app_id` when given and the
908    /// configured id otherwise, and without the no-alert marker: this is the
909    /// builder for user content, which the relay should push.
910    pub(super) fn create_message_for_app(
911        &mut self,
912        recipient: impl Into<String>,
913        content: impl Into<String>,
914        priority: Option<MessagePriority>,
915        reply_to_msg: Option<MessageId>,
916        app_id: Option<AppId>,
917    ) -> Result<Message> {
918        let sender = UserId::new(&self.local_id)?;
919        let recipient = UserId::new(recipient)?;
920        let app_id = match app_id {
921            Some(app_id) => app_id,
922            None => AppId::new(&self.config.app_id)?,
923        };
924
925        let clock_value = self.lamport_clock.tick();
926        self.persist_lamport_clock();
927
928        let mut builder = Message::builder(sender, recipient, app_id)
929            .content(content)
930            .priority(priority.unwrap_or(MessagePriority::Medium))
931            .ttl(TTL::new(self.config.initial_ttl)?)
932            .lamport_clock(clock_value);
933
934        if let Some(reply_to) = reply_to_msg {
935            builder = builder.reply_to_msg(reply_to);
936        }
937
938        Ok(builder.build())
939    }
940
941    // ========================================================================
942    // ENCRYPTION
943    // ========================================================================
944
945    pub(super) fn encrypt_content_for_recipient(
946        &mut self,
947        recipient: &str,
948        content: &str,
949        _priority: MessagePriority,
950    ) -> Result<String> {
951        let encrypted = self.encrypt_bytes_for_recipient(recipient, content.as_bytes())?;
952        self.seal_encrypted_content(recipient, &encrypted)
953    }
954
955    /// Bytes-level core of [`Self::encrypt_content_for_recipient`]: initiates
956    /// session establishment when a key package is available and encrypts once
957    /// the session is confirmed. Text content and media chunk plaintexts share
958    /// this path.
959    pub(super) fn encrypt_bytes_for_recipient(
960        &mut self,
961        recipient: &str,
962        plaintext: &[u8],
963    ) -> Result<EncryptedMessage> {
964        // Clone the Arc to avoid borrow issues
965        let mls = self.mls_manager.clone().ok_or_else(|| {
966            self.emit_mls_session_missing(
967                Some(recipient),
968                None,
969                MlsOperationContext::SessionLookup,
970                MlsErrorCategory::NotInitialized,
971            );
972            Error::MlsNotInitialized
973        })?;
974
975        // Fast path: if the session is already confirmed (in-memory cache hit),
976        // skip the has_session() storage call — we know the session exists.
977        if self.confirmed_sessions.contains(recipient) {
978            return self.encrypt_bytes_confirmed_session(&mls, recipient, plaintext);
979        }
980
981        // Every caller is a text send, whose message queues behind the
982        // Welcome; the session-confirm marker also lands here, but only
983        // after adopting a session, so it never creates one.
984        self.ensure_session_establishment(&mls, recipient, true)?;
985
986        // Only encrypt if session is confirmed (Welcome processed or successful decrypt).
987        // Confirmation truth comes from persisted session state.
988        if !self.is_session_confirmed(recipient)? {
989            debug!(recipient = %recipient, "Session exists but not confirmed, queuing message");
990            return Err(Error::SessionNotReady(self.establishment_state(recipient)?));
991        }
992
993        // Encrypt the message
994        let encrypted = {
995            let manager = mls
996                .read()
997                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
998            manager
999                .encrypt_for_user(recipient, plaintext)
1000                .map_err(|_| Error::EncryptFailed("encryption operation failed".to_string()))?
1001        };
1002
1003        self.emit_mls_encryption_used(recipient);
1004        Ok(encrypted)
1005    }
1006
1007    /// Ensures MLS session establishment with `recipient` has at least been
1008    /// initiated: when no session exists in storage, imports a stored/pending
1009    /// key package, creates the session, and sends the Welcome.
1010    ///
1011    /// Returns `Ok(())` when a session already exists (possibly unconfirmed —
1012    /// callers must still gate on `is_session_confirmed`) or when no usable
1013    /// key package remains (expired). Returns `Err(SessionNotReady)` when no
1014    /// key package is available, or right after creating a session with
1015    /// `store_pending` enabled (the session cannot be confirmed yet).
1016    ///
1017    /// `user_send` is whether something the user sent prompted this, and so
1018    /// whether a Welcome it sends may alert (see
1019    /// [`Self::send_welcome_message`]).
1020    pub(super) fn ensure_session_establishment(
1021        &mut self,
1022        mls: &Arc<RwLock<MlsManager>>,
1023        recipient: &str,
1024        user_send: bool,
1025    ) -> Result<()> {
1026        // Check for existing session (requires storage I/O via load_group)
1027        let has_session = {
1028            let manager = mls
1029                .read()
1030                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
1031            manager.has_session(recipient)?
1032        };
1033
1034        if !has_session {
1035            // Try loading key package from storage (e.g. after restart) then create session from memory
1036            self.try_load_key_package_from_storage_into_memory(recipient);
1037            // Try to create session from stored key package
1038            // Clone first, only remove after all operations succeed to avoid losing the key package on failure
1039            if let Some(received_pkg) = self.pending_key_packages.get(recipient).cloned() {
1040                // Check if key package has expired (using local clock)
1041                let now_ms = Utc::now().timestamp_millis() as u64;
1042                if now_ms >= received_pkg.local_expires_at_ms {
1043                    warn!(recipient = %recipient, "Received key package has expired, discarding");
1044                    self.pending_key_packages.remove(recipient);
1045                    self.delete_peer_key_package_from_storage(recipient);
1046                } else {
1047                    self.import_pending_key_package(
1048                        mls,
1049                        recipient,
1050                        &received_pkg.key_package_data,
1051                    )?;
1052
1053                    // Create session and send welcome message
1054                    let welcome = {
1055                        let manager = mls
1056                            .read()
1057                            .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
1058                        manager.create_session(recipient)?
1059                    };
1060                    // The handshake starts here, and this is the branch it
1061                    // usually starts on: with `auto_key_exchange` the peer's
1062                    // key package has normally arrived already, so the `else`
1063                    // below never runs and no `session_missing` is raised.
1064                    // Without this emit the latency percentile would see only
1065                    // the handshakes that had to wait for a key package.
1066                    self.emit_mls_session_establishing(
1067                        recipient,
1068                        welcome.group_id.as_str(),
1069                        MlsOperationContext::Welcome,
1070                    );
1071                    self.mark_encryption_capable(recipient);
1072
1073                    // All operations succeeded, now safe to remove the key package
1074                    self.pending_key_packages.remove(recipient);
1075                    self.delete_peer_key_package_from_storage(recipient);
1076
1077                    let group_id = welcome.group_id.as_str().to_string();
1078                    let is_session = group_id.starts_with("session:");
1079
1080                    if let Err(err) =
1081                        self.ensure_session_state_entry(recipient, "session_created_local")
1082                    {
1083                        warn!(
1084                            recipient = %recipient,
1085                            error = %err,
1086                            "Failed to persist pending session state"
1087                        );
1088                    }
1089
1090                    let welcome_sent = self.send_welcome_message(recipient, &welcome, user_send)?;
1091
1092                    debug!(
1093                        recipient = %recipient,
1094                        group_id = %group_id,
1095                        welcome_sent = welcome_sent,
1096                        "Created MLS session and scheduled welcome lifecycle"
1097                    );
1098
1099                    if welcome_sent {
1100                        debug!(recipient = %recipient, group_id = %group_id, is_session, "Welcome synchronously sent");
1101                    }
1102
1103                    // Don't encrypt immediately after creating session.
1104                    // Queue message until session is confirmed (peer processes our Welcome
1105                    // and we successfully decrypt their first message, or we receive their Welcome).
1106                    // This avoids race conditions where both peers create sessions.
1107                    if self.config.encryption.store_pending {
1108                        return Err(Error::SessionNotReady(self.establishment_state(recipient)?));
1109                    }
1110                }
1111            } else {
1112                // No key package available (memory nor storage)
1113                self.emit_mls_session_missing(
1114                    Some(recipient),
1115                    None,
1116                    MlsOperationContext::SessionLookup,
1117                    MlsErrorCategory::SessionStateMissing,
1118                );
1119                return Err(Error::SessionNotReady(self.establishment_state(recipient)?));
1120            }
1121        }
1122
1123        Ok(())
1124    }
1125
1126    /// Serializes an [`EncryptedMessage`] into the prefixed string form used
1127    /// by the text path.
1128    ///
1129    /// Recipients that advertised [`MLS_ENVELOPE_COMPACT_V1`] in their key
1130    /// package (with our own `compact_envelope_enabled` on) get the compact
1131    /// envelope — `__MLS_ENC__` + base64 of [`EncryptedMessage::to_bytes`] —
1132    /// roughly 2.7x smaller than the legacy JSON form, whose `ciphertext`
1133    /// field renders as an integer array. Everyone else gets `__MLS_ENC__` +
1134    /// JSON, the permanent floor. Receivers sniff the byte after the prefix
1135    /// (`{` = JSON), so no per-message signaling is needed, and the choice is
1136    /// per-recipient end-to-end state, valid however the message is later
1137    /// routed or retried.
1138    pub(super) fn seal_encrypted_content(
1139        &self,
1140        recipient: &str,
1141        encrypted: &EncryptedMessage,
1142    ) -> Result<String> {
1143        if self.config.encryption.compact_envelope_enabled
1144            && self.peer_compact_envelope.contains(recipient)
1145        {
1146            return Ok(format!(
1147                "{}{}",
1148                internal_prefixes::ENCRYPTED,
1149                base64_encode(&encrypted.to_bytes())
1150            ));
1151        }
1152        let serialized =
1153            serde_json::to_string(encrypted).map_err(|e| Error::Serialization(e.to_string()))?;
1154        Ok(format!("{}{}", internal_prefixes::ENCRYPTED, serialized))
1155    }
1156
1157    /// The sync versions this device advertises.
1158    ///
1159    /// Empty unless the data layer is both compiled in and switched on. A
1160    /// capability advertised with nothing behind it is worse than silence:
1161    /// the peer starts sending frames and waiting for answers that never
1162    /// come, and nothing in that exchange looks like an error to either
1163    /// side.
1164    pub(crate) fn advertised_data_versions(&self) -> Vec<u8> {
1165        #[cfg(feature = "data")]
1166        {
1167            if self.config.data.enabled {
1168                return vec![
1169                    super::types::DATA_SYNC_V1,
1170                    super::types::DATA_GROUP_V1,
1171                    super::types::DATA_MEDIA_V1,
1172                    super::types::DATA_TOMBSTONE_V1,
1173                    super::types::DATA_INTEREST_V1,
1174                    super::types::DATA_GROUP_BLOB_V1,
1175                    super::types::DATA_CUSTODY_V1,
1176                ];
1177            }
1178        }
1179        Vec::new()
1180    }
1181
1182    /// Whether document sync is live toward `recipient`: the layer is
1183    /// compiled in and enabled here, the peer is not blocked, and they
1184    /// advertised [`DATA_SYNC_V1`] in their key package.
1185    ///
1186    /// Consulted per send rather than cached at session establishment, like
1187    /// the two capability gates below it: a peer that downgrades stops
1188    /// receiving frames on their next key package, not at their next
1189    /// session.
1190    ///
1191    /// The block check is here rather than at the call sites because this is
1192    /// the only gate they share, and because replication is not reached
1193    /// through a send API: every public send surface checks blocking for
1194    /// itself, while these frames are triggered by a commit or a discovery.
1195    /// A blocked peer still holds a live session, so nothing further down
1196    /// would have refused the frame. Inbound needs no equivalent: the
1197    /// receive path drops a blocked sender before any dispatch.
1198    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1199    pub(super) fn data_sync_active(&self, recipient: &str) -> bool {
1200        #[cfg(feature = "data")]
1201        {
1202            self.config.data.enabled
1203                && self.peer_data_sync.contains(recipient)
1204                && !self.is_user_blocked(recipient)
1205        }
1206        #[cfg(not(feature = "data"))]
1207        {
1208            let _ = recipient;
1209            false
1210        }
1211    }
1212
1213    /// Whether blobs may travel to `recipient`: 1:1 replication is live with
1214    /// them and they advertised [`DATA_MEDIA_V1`], so they understand the
1215    /// fetch frames and route a data-purposed media transfer into the data
1216    /// layer rather than to their user.
1217    ///
1218    /// Strictly narrower than [`Self::data_sync_active`] and checked in
1219    /// addition to it, never instead: every blob exchange is part of a
1220    /// replication that the first gate already had to allow, and a peer who
1221    /// stops replicating stops receiving blobs at the same moment.
1222    ///
1223    /// [`DATA_MEDIA_V1`]: crate::protocol::types::DATA_MEDIA_V1
1224    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1225    pub(super) fn data_media_active(&self, recipient: &str) -> bool {
1226        #[cfg(feature = "data")]
1227        {
1228            self.data_sync_active(recipient) && self.peer_data_media.contains(recipient)
1229        }
1230        #[cfg(not(feature = "data"))]
1231        {
1232            let _ = recipient;
1233            false
1234        }
1235    }
1236
1237    /// Whether removals may be carried toward `recipient`: 1:1 replication
1238    /// is live with them and they advertised [`DATA_TOMBSTONE_V1`], so they
1239    /// read the `gone` field rather than ignoring it.
1240    ///
1241    /// Unlike the two gates above it, this one guards traffic rather than
1242    /// correctness. A peer without the entry ignores the field, keeps its
1243    /// copy and re-offers it; this device refuses the offer and any blob
1244    /// behind it against its own floor, so the removal holds here either
1245    /// way. Sending the field anyway would be harmless and useless, and
1246    /// leaving it out keeps that exchange as small as it can be.
1247    ///
1248    /// [`DATA_TOMBSTONE_V1`]: crate::protocol::types::DATA_TOMBSTONE_V1
1249    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1250    pub(super) fn data_tombstones_active(&self, recipient: &str) -> bool {
1251        #[cfg(feature = "data")]
1252        {
1253            self.data_sync_active(recipient) && self.peer_data_tombstones.contains(recipient)
1254        }
1255        #[cfg(not(feature = "data"))]
1256        {
1257            let _ = recipient;
1258            false
1259        }
1260    }
1261
1262    /// Whether a narrowed interest may be declared toward `recipient`: 1:1
1263    /// replication is live with them and they advertised
1264    /// [`DATA_INTEREST_V1`], so they answer inside it rather than ignoring
1265    /// it.
1266    ///
1267    /// A traffic gate, like the one above it. A peer without the entry sends
1268    /// everything it holds and this device refuses what it did not ask for,
1269    /// so a narrowed space is narrow either way; what the entry buys is the
1270    /// bytes never leaving the peer.
1271    ///
1272    /// [`DATA_INTEREST_V1`]: crate::protocol::types::DATA_INTEREST_V1
1273    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1274    pub(super) fn data_interest_active(&self, recipient: &str) -> bool {
1275        #[cfg(feature = "data")]
1276        {
1277            self.data_sync_active(recipient) && self.peer_data_interest.contains(recipient)
1278        }
1279        #[cfg(not(feature = "data"))]
1280        {
1281            let _ = recipient;
1282            false
1283        }
1284    }
1285
1286    /// Whether `peer` is known to intercept replication frames inside a
1287    /// group ([`DATA_GROUP_V1`]), either directly or by an inviter's
1288    /// attestation.
1289    ///
1290    /// Positive knowledge, and the opposite rule to its blob sibling below,
1291    /// because it answers a different question. This one decides whether a
1292    /// `__DATA_V1__` frame may be *put in front of* a member: one that does
1293    /// not intercept them renders the frame to its user as literal text, so
1294    /// an unknown member closes this and an application is told at the call.
1295    /// The blob sibling decides whether a member can usefully answer a
1296    /// question they will certainly receive, where the honest answer to "we
1297    /// have not heard" is to ask.
1298    ///
1299    /// The roster-wide gate is this predicate over every member, which is
1300    /// not a coincidence to be refactored apart: a directed frame can be
1301    /// promoted to a roster-wide delivery, so the two must agree about what
1302    /// a member has to be known to do before a frame reaches them.
1303    ///
1304    /// [`DATA_GROUP_V1`]: crate::protocol::types::DATA_GROUP_V1
1305    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1306    pub(super) fn data_group_frames_known_received(&self, peer: &str) -> bool {
1307        #[cfg(feature = "data")]
1308        {
1309            self.peer_data_group.contains(peer) || self.peer_data_group_attested.contains(peer)
1310        }
1311        #[cfg(not(feature = "data"))]
1312        {
1313            let _ = peer;
1314            false
1315        }
1316    }
1317
1318    /// Whether `peer` is *known* not to carry attachment bytes inside a
1319    /// group.
1320    ///
1321    /// Knowledge, never its absence. Members of a group do not exchange key
1322    /// packages with each other, so "we have not heard" is the ordinary case
1323    /// and must not read as "they cannot": that would refuse every fetch put
1324    /// to a member this device has never dealt with directly. An inviter's
1325    /// attestation fills the gap where there is one.
1326    ///
1327    /// [`DATA_GROUP_BLOB_V1`]: crate::protocol::types::DATA_GROUP_BLOB_V1
1328    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1329    pub(super) fn data_group_blob_known_absent(&self, peer: &str) -> bool {
1330        #[cfg(feature = "data")]
1331        {
1332            // Any direct knowledge of what this peer replicates, not just
1333            // the first entry. A key package carries the whole list at
1334            // once, so a peer in one of these sets is one whose
1335            // advertisement this device has actually read, and the blob
1336            // entry's absence from it is knowledge rather than silence.
1337            // Reading entry 1 alone would make a peer known only by a
1338            // later entry look like one never heard from, and the refusal
1339            // this predicate exists to make would never fire for them.
1340            let heard_directly =
1341                self.peer_data_sync.contains(peer) || self.peer_data_group.contains(peer);
1342            let attested = self.peer_data_group_blob_attested.contains(peer);
1343            let carries = self.peer_data_group_blob.contains(peer) || attested;
1344            (heard_directly || attested) && !carries
1345        }
1346        #[cfg(not(feature = "data"))]
1347        {
1348            let _ = peer;
1349            false
1350        }
1351    }
1352
1353    /// Whether rich extras seal for `recipient`: our own kill switch is on
1354    /// and the peer advertised [`RICH_PAYLOAD_V1`] in their key package.
1355    pub(super) fn rich_seal_active(&self, recipient: &str) -> bool {
1356        self.config.encryption.rich_payload_enabled && self.peer_rich_payload.contains(recipient)
1357    }
1358
1359    /// Whether rich extras may seal into a group message: our own kill
1360    /// switch is on and every non-self member is known to parse
1361    /// [`RICH_PAYLOAD_V1`] — either self-advertised in a directly received
1362    /// key package (`peer_rich_payload`) or attested by a group inviter on
1363    /// the Add commit / Welcome (`peer_rich_attested`; members added by
1364    /// someone else never exchange key packages with us directly). Group
1365    /// MLS encryption produces a single ciphertext for all members, so one
1366    /// non-capable or capability-unknown member forces the extras to drop
1367    /// for the whole group. Conservative by design: an unknown member must
1368    /// never receive a sealed body their SDK would render as literal
1369    /// `__RICH_V1__` JSON.
1370    ///
1371    /// Attestation is third-party and may be stale or forged by a rogue
1372    /// admin; the worst case is exactly the degraded display below, never a
1373    /// leak, and direct contact evicts the attested entry. Best-effort
1374    /// beyond that: callers pass the `group_mesh.members` fan-out cache,
1375    /// which trails MLS membership until the add/remove notification
1376    /// lands. In that window a just-added non-capable member can receive
1377    /// one sealed body and render it as literal JSON — degraded display,
1378    /// not a leak (every group member is entitled to the sealed contents).
1379    pub(crate) fn group_rich_seal_active(&self, members: &[String]) -> bool {
1380        self.config.encryption.rich_payload_enabled
1381            && members
1382                .iter()
1383                .filter(|m| m.as_str() != self.local_id)
1384                .all(|m| {
1385                    self.peer_rich_payload.contains(m.as_str())
1386                        || self.peer_rich_attested.contains(m.as_str())
1387                })
1388    }
1389
1390    /// Group members (non-self) not known to parse the sealed rich payload
1391    /// — neither directly advertised nor inviter-attested. These are the
1392    /// members that hold `group_rich_seal_active` closed; the set absence
1393    /// cannot distinguish "never heard from" from "known not capable".
1394    pub(crate) fn group_rich_unknown_members(&self, members: &[String]) -> Vec<String> {
1395        members
1396            .iter()
1397            .filter(|m| {
1398                m.as_str() != self.local_id
1399                    && !self.peer_rich_payload.contains(m.as_str())
1400                    && !self.peer_rich_attested.contains(m.as_str())
1401            })
1402            .cloned()
1403            .collect()
1404    }
1405
1406    /// The rich-payload versions we can attest for a peer when inviting it
1407    /// into (or welcoming it to) a group: [`RICH_PAYLOAD_V1`] when the peer
1408    /// is known capable (directly or itself attested — attestation chains
1409    /// transitively, which is how knowledge reaches members several adds
1410    /// removed from any direct exchange), `None` when unknown. `None` is
1411    /// deliberately not a downgrade signal: absence of knowledge must never
1412    /// evict what another member learned first-hand.
1413    pub(crate) fn attestable_rich_versions(&self, peer_id: &str) -> Option<Vec<u8>> {
1414        (self.peer_rich_payload.contains(peer_id) || self.peer_rich_attested.contains(peer_id))
1415            .then(|| vec![RICH_PAYLOAD_V1])
1416    }
1417
1418    /// Whether documents may replicate into a group: the layer is enabled
1419    /// here and every non-self member is known to intercept group sync
1420    /// frames — either self-advertised in a directly received key package
1421    /// (`peer_data_group`) or attested by a group inviter on the Add commit
1422    /// / Welcome (`peer_data_group_attested`).
1423    ///
1424    /// All-members rather than per-member, and that is not a convenience.
1425    /// Group MLS encryption produces one ciphertext for everyone, so a
1426    /// frame sent for the capable members is also delivered to the rest;
1427    /// one member that cannot intercept it renders it to its user as
1428    /// literal `__DATA_V1__` text. The gate is therefore closed by a single
1429    /// unknown member, which is the same conservatism
1430    /// [`Self::group_rich_seal_active`] applies to sealed rich bodies, for
1431    /// the same reason and with a worse symptom if skipped.
1432    ///
1433    /// Blocking is deliberately not consulted here. A blocked *peer* is a
1434    /// 1:1 relationship; a group send is one ciphertext to a roster, and
1435    /// dropping the whole group's replication because one member is blocked
1436    /// would be a surprising amount of collateral. The 1:1 gate keeps its
1437    /// block check because there the recipient and the relationship are the
1438    /// same thing.
1439    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1440    pub(crate) fn group_data_sync_active(&self, members: &[String]) -> bool {
1441        #[cfg(feature = "data")]
1442        {
1443            self.config.data.enabled
1444                && members
1445                    .iter()
1446                    .filter(|m| m.as_str() != self.local_id)
1447                    .all(|m| self.data_group_frames_known_received(m.as_str()))
1448        }
1449        #[cfg(not(feature = "data"))]
1450        {
1451            let _ = members;
1452            false
1453        }
1454    }
1455
1456    /// Group members (non-self) not known to intercept group sync frames.
1457    ///
1458    /// These are the members holding [`Self::group_data_sync_active`]
1459    /// closed. Handed to [`Self::backfill_group_rich_capabilities`], whose
1460    /// probe is capability-agnostic: it sends our key package so the peer's
1461    /// reply teaches us everything they advertise at once.
1462    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1463    pub(crate) fn group_data_unknown_members(&self, members: &[String]) -> Vec<String> {
1464        #[cfg(feature = "data")]
1465        {
1466            members
1467                .iter()
1468                .filter(|m| {
1469                    m.as_str() != self.local_id
1470                        && !self.peer_data_group.contains(m.as_str())
1471                        && !self.peer_data_group_attested.contains(m.as_str())
1472                })
1473                .cloned()
1474                .collect()
1475        }
1476        #[cfg(not(feature = "data"))]
1477        {
1478            let _ = members;
1479            Vec::new()
1480        }
1481    }
1482
1483    /// The sync versions we can attest for a peer when inviting it into (or
1484    /// welcoming it to) a group.
1485    ///
1486    /// [`DATA_GROUP_V1`] when the peer is known group-capable, directly or
1487    /// itself attested — attestation chains transitively, which is how
1488    /// knowledge reaches members several adds removed from any direct
1489    /// exchange. `None` when unknown, and `None` is deliberately not a
1490    /// downgrade signal: absence of knowledge must never evict what another
1491    /// member learned first-hand.
1492    ///
1493    /// [`DATA_GROUP_V1`]: crate::protocol::types::DATA_GROUP_V1
1494    #[cfg_attr(not(feature = "data"), allow(dead_code))]
1495    pub(crate) fn attestable_data_versions(&self, peer_id: &str) -> Option<Vec<u8>> {
1496        #[cfg(feature = "data")]
1497        {
1498            // Every group-relevant entry this device knows about the peer,
1499            // rather than the one it used to be. An inviter is the only
1500            // source a member has for another member, so anything it leaves
1501            // out is something nobody else can supply.
1502            let mut versions = Vec::new();
1503            if self.peer_data_group.contains(peer_id)
1504                || self.peer_data_group_attested.contains(peer_id)
1505            {
1506                versions.push(super::types::DATA_GROUP_V1);
1507            }
1508            if self.peer_data_group_blob.contains(peer_id)
1509                || self.peer_data_group_blob_attested.contains(peer_id)
1510            {
1511                versions.push(super::types::DATA_GROUP_BLOB_V1);
1512            }
1513            // Entry 2 is what makes a member reachable at all, so an
1514            // attestation without it says nothing usable and is not sent.
1515            versions
1516                .first()
1517                .is_some_and(|first| *first == super::types::DATA_GROUP_V1)
1518                .then_some(versions)
1519        }
1520        #[cfg(not(feature = "data"))]
1521        {
1522            let _ = peer_id;
1523            None
1524        }
1525    }
1526
1527    /// Best-effort capability backfill for group members whose rich support
1528    /// is unknown (pre-attestation groups, or an attestation chain broken by
1529    /// an old-SDK inviter): send them our key package so their
1530    /// `auto_key_exchange` reply teaches us theirs. Guarded by
1531    /// `key_package_sent_to`, so repeated gate-failing group sends don't
1532    /// re-probe the same peer (the only repeat is
1533    /// `rearm_key_package_for_peer`, on its doubling backoff); a peer that
1534    /// never replies is either
1535    /// unreachable, old-SDK, or opted out — all of which correctly leave
1536    /// the gate closed.
1537    pub(crate) fn backfill_group_rich_capabilities(&mut self, unknown_members: &[String]) {
1538        for member in unknown_members {
1539            if self.key_package_sent_to.contains_key(member.as_str()) {
1540                continue;
1541            }
1542            if let Err(e) = self.send_key_package_to(member, false) {
1543                debug!(
1544                    member = %member,
1545                    error = %e,
1546                    "Rich-capability backfill key package deferred (peer unreachable)"
1547                );
1548            }
1549        }
1550    }
1551
1552    /// Wraps plaintext content and rich extras into the sealed
1553    /// `__RICH_V1__`-prefixed JSON body ([`RichPayloadV1`]). Callers must
1554    /// only invoke this on a path that MLS-encrypts the result — the sealed
1555    /// body is the sole carrier for reply context and media secrets and must
1556    /// never leave as cleartext.
1557    ///
1558    /// The outer `content_type` hint is always copied into the body (not
1559    /// just when non-Text) so the receiver can treat the sealed copy as
1560    /// authoritative and a relay cannot rewrite the hint in either
1561    /// direction on a rich message.
1562    pub(crate) fn seal_rich_payload(
1563        content: &str,
1564        extras: &RichSendExtras,
1565        content_type: ContentType,
1566    ) -> Result<String> {
1567        let payload = RichPayloadV1 {
1568            text: content.to_string(),
1569            reply_context: extras.reply_context.clone(),
1570            media_metadata: extras.media_metadata.clone(),
1571            forward_info: extras.forward_info.clone(),
1572            content_type: Some(content_type),
1573        };
1574        let serialized =
1575            serde_json::to_string(&payload).map_err(|e| Error::Serialization(e.to_string()))?;
1576        Ok(format!("{}{}", internal_prefixes::RICH_V1, serialized))
1577    }
1578
1579    /// Encrypts a plaintext for a recipient whose session is known to be
1580    /// confirmed (in-memory cache hit). Uses `encrypt_for_existing_session` to
1581    /// skip both the external `has_session()` and the internal one inside
1582    /// `encrypt_for_user`, reducing storage I/O from 2 round-trips to 1
1583    /// (`load_group` for encrypt).
1584    ///
1585    /// Only evicts the cache on `SessionNotFound` (session deleted externally).
1586    /// Transient errors (crypto, storage I/O) propagate without cache eviction
1587    /// so the fast path is preserved for the next attempt.
1588    pub(super) fn encrypt_bytes_confirmed_session(
1589        &mut self,
1590        mls: &Arc<RwLock<MlsManager>>,
1591        recipient: &str,
1592        plaintext: &[u8],
1593    ) -> Result<EncryptedMessage> {
1594        let encrypt_result = {
1595            let manager = mls
1596                .read()
1597                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
1598            manager.encrypt_for_existing_session(recipient, plaintext)
1599        };
1600
1601        let encrypted = match encrypt_result {
1602            Ok(enc) => enc,
1603            Err(offline_protocol_mls::MlsError::SessionNotFound(_)) => {
1604                // Session was deleted externally — evict stale cache entry and
1605                // return SessionNotReady so the send pipeline can queue the
1606                // message (when store_pending is enabled) rather than dropping it.
1607                warn!(
1608                    recipient = %recipient,
1609                    "Confirmed session missing from MLS storage, evicting cache"
1610                );
1611                self.confirmed_sessions.remove(recipient);
1612                let state = self
1613                    .establishment_state(recipient)
1614                    .unwrap_or(crate::EstablishmentState::NoKeyPackage);
1615                return Err(Error::SessionNotReady(state));
1616            }
1617            Err(e) => {
1618                // Transient error (crypto, storage I/O, etc.) — do NOT evict
1619                // cache, the session likely still exists.
1620                return Err(Error::EncryptFailed(format!(
1621                    "encryption operation failed: {}",
1622                    e
1623                )));
1624            }
1625        };
1626
1627        self.emit_mls_encryption_used(recipient);
1628        Ok(encrypted)
1629    }
1630
1631    pub(super) fn encrypt_content_for_recipient_strict(
1632        &mut self,
1633        recipient: &str,
1634        content: &str,
1635    ) -> Result<String> {
1636        let encrypted = self.encrypt_bytes_for_recipient_strict(recipient, content.as_bytes())?;
1637        self.seal_encrypted_content(recipient, &encrypted)
1638    }
1639
1640    /// Bytes-level core of [`Self::encrypt_content_for_recipient_strict`]:
1641    /// requires an existing confirmed session and never initiates
1642    /// establishment.
1643    pub(super) fn encrypt_bytes_for_recipient_strict(
1644        &mut self,
1645        recipient: &str,
1646        plaintext: &[u8],
1647    ) -> Result<EncryptedMessage> {
1648        let mls = self.mls_manager.clone().ok_or_else(|| {
1649            self.emit_mls_session_missing(
1650                Some(recipient),
1651                None,
1652                MlsOperationContext::SessionLookup,
1653                MlsErrorCategory::NotInitialized,
1654            );
1655            Error::MlsNotInitialized
1656        })?;
1657
1658        // Fast path: if session is confirmed (in-memory cache), skip has_session() I/O
1659        if self.confirmed_sessions.contains(recipient) {
1660            return self.encrypt_bytes_confirmed_session(&mls, recipient, plaintext);
1661        }
1662
1663        let has_session = {
1664            let manager = mls
1665                .read()
1666                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
1667            manager.has_session(recipient)?
1668        };
1669
1670        if !has_session {
1671            self.try_load_key_package_from_storage_into_memory(recipient);
1672            let now_ms = Utc::now().timestamp_millis() as u64;
1673            let has_valid_key_package = match self.pending_key_packages.get(recipient) {
1674                Some(pkg) if now_ms < pkg.local_expires_at_ms => true,
1675                Some(_) => {
1676                    self.pending_key_packages.remove(recipient);
1677                    self.delete_peer_key_package_from_storage(recipient);
1678                    false
1679                }
1680                None => false,
1681            };
1682
1683            if has_valid_key_package {
1684                return Err(Error::SessionNotReady(self.establishment_state(recipient)?));
1685            }
1686
1687            self.emit_mls_session_missing(
1688                Some(recipient),
1689                None,
1690                MlsOperationContext::SessionLookup,
1691                MlsErrorCategory::SessionStateMissing,
1692            );
1693            return Err(Error::SessionNotReady(self.establishment_state(recipient)?));
1694        }
1695
1696        if !self.is_session_confirmed(recipient)? {
1697            return Err(Error::SessionNotReady(self.establishment_state(recipient)?));
1698        }
1699
1700        let encrypted = {
1701            let manager = mls
1702                .read()
1703                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
1704            manager
1705                .encrypt_for_user(recipient, plaintext)
1706                .map_err(|_| Error::EncryptFailed("encryption operation failed".to_string()))?
1707        };
1708
1709        self.emit_mls_encryption_used(recipient);
1710        Ok(encrypted)
1711    }
1712
1713    #[allow(clippy::too_many_arguments)]
1714    pub(super) fn prepare_outbound_content(
1715        &mut self,
1716        recipient: &str,
1717        content: &str,
1718        priority: MessagePriority,
1719        reply_to_msg_id: Option<MessageId>,
1720        forwarded_from: Option<ForwardInfo>,
1721        content_type: ContentType,
1722        media_metadata: Option<MediaMetadata>,
1723        rich: Option<&RichSendExtras>,
1724        app_id: Option<&AppId>,
1725        provenance: Option<PendingProvenance>,
1726        reconciliation_reason: &'static str,
1727    ) -> Result<OutboundSendPreparation> {
1728        if self.should_auto_encrypt() {
1729            // Rich extras travel only inside the sealed body: seal for a
1730            // capable recipient, drop for everyone else — never cleartext.
1731            // The decision is made here, at actual-send time; a message
1732            // queued behind session establishment stores the raw extras and
1733            // re-evaluates capability at flush (the key package that
1734            // confirms the session may be the one that advertised it).
1735            let rich_extras = rich.filter(|extras| extras.is_any());
1736            let sealed_body;
1737            let outbound: &str = match rich_extras {
1738                Some(extras) if self.rich_seal_active(recipient) => {
1739                    sealed_body = Self::seal_rich_payload(content, extras, content_type)?;
1740                    &sealed_body
1741                }
1742                Some(_) => {
1743                    debug!(
1744                        recipient = %recipient,
1745                        "Recipient lacks sealed rich payload capability, dropping rich extras"
1746                    );
1747                    content
1748                }
1749                // No rich extras, but a non-Text content_type toward a
1750                // capable recipient still seals a hint-only body: the outer
1751                // hint is relay-writable, and restamping it FileChunk gets
1752                // the decrypted message ACKed and then dropped by the
1753                // file-transfer manager — worse than a plain relay drop,
1754                // which at least fails the ACK and retries. Only when
1755                // nothing rides outer (`forwarded_from`/`media_metadata`
1756                // both absent): fresh forwards thread their attribution and
1757                // media metadata through `rich` above, so this outer-only
1758                // shape survives solely in messages queued by an older build
1759                // (persisted `PendingMessage`s with no `rich`), where the
1760                // receiver's wholesale sealed-body restore would wipe the
1761                // outer copies a hint-only seal can't replicate. Bare Text
1762                // seals nothing: no hint to protect, and the unsealed form
1763                // keeps the plaintext floor maximal.
1764                None if content_type != ContentType::Text
1765                    && forwarded_from.is_none()
1766                    && media_metadata.is_none()
1767                    && self.rich_seal_active(recipient) =>
1768                {
1769                    sealed_body =
1770                        Self::seal_rich_payload(content, &RichSendExtras::default(), content_type)?;
1771                    &sealed_body
1772                }
1773                None => content,
1774            };
1775            if self.config.encryption.require_encryption {
1776                match self.encrypt_content_for_recipient_strict(recipient, outbound) {
1777                    Ok(encrypted) => return Ok(OutboundSendPreparation::Ready(encrypted)),
1778                    Err(Error::SessionNotReady(_)) if self.config.encryption.store_pending => {
1779                        // Session not ready but store_pending is enabled — queue
1780                        // the message so it gets encrypted and sent once the
1781                        // session is confirmed, rather than dropping it.
1782                        //
1783                        // Kick establishment first (import a stored key package,
1784                        // create the session, send the Welcome), matching the
1785                        // non-strict and media paths: without this a peer whose
1786                        // key package arrived but whose arrival-time
1787                        // auto-establish failed would stall until the peer
1788                        // initiates. SessionNotReady from the kick is the
1789                        // expected "created, awaiting confirmation" outcome;
1790                        // the message queues regardless.
1791                        if let Some(mls) = self.mls_manager.clone() {
1792                            match self.ensure_session_establishment(&mls, recipient, true) {
1793                                Ok(()) | Err(Error::SessionNotReady(_)) => {}
1794                                Err(err) => {
1795                                    warn!(
1796                                        recipient = %recipient,
1797                                        error = %err,
1798                                        "Session establishment kick failed; message queued anyway"
1799                                    );
1800                                }
1801                            }
1802                        }
1803                        let queued_id = self.queue_message_for_session_establishment(
1804                            recipient,
1805                            content,
1806                            priority,
1807                            reply_to_msg_id,
1808                            forwarded_from,
1809                            content_type,
1810                            media_metadata,
1811                            rich_extras.cloned(),
1812                            app_id,
1813                            provenance,
1814                            reconciliation_reason,
1815                        )?;
1816                        return Ok(OutboundSendPreparation::Queued(queued_id));
1817                    }
1818                    Err(e) => return Err(e),
1819                }
1820            }
1821
1822            match self.encrypt_content_for_recipient(recipient, outbound, priority) {
1823                Ok(encrypted) => Ok(OutboundSendPreparation::Ready(encrypted)),
1824                Err(Error::SessionNotReady(state)) => {
1825                    if !self.config.encryption.store_pending {
1826                        return Err(Error::SessionNotReady(state));
1827                    }
1828
1829                    let queued_id = self.queue_message_for_session_establishment(
1830                        recipient,
1831                        content,
1832                        priority,
1833                        reply_to_msg_id,
1834                        forwarded_from,
1835                        content_type,
1836                        media_metadata,
1837                        rich_extras.cloned(),
1838                        app_id,
1839                        provenance,
1840                        reconciliation_reason,
1841                    )?;
1842                    Ok(OutboundSendPreparation::Queued(queued_id))
1843                }
1844                Err(e) => Err(e),
1845            }
1846        } else if self.config.encryption.require_encryption {
1847            Err(Error::EncryptFailed(
1848                "MLS encryption is required (the default) but MLS is not initialized — \
1849                 call initialize_mls() with secure and protocol-state storage, or explicitly opt out with \
1850                 require_encryption=false to send plaintext"
1851                    .to_string(),
1852            ))
1853        } else {
1854            // Explicit opt-out: require_encryption=false with encryption
1855            // disabled or MLS uninitialized. The message leaves as plaintext;
1856            // surface that loudly (once per peer) instead of a debug log.
1857            // Rich extras are dropped here unconditionally — they only ever
1858            // travel inside the sealed body, never as cleartext.
1859            if rich.is_some_and(|extras| extras.is_any()) {
1860                debug!(
1861                    recipient = %recipient,
1862                    "Plaintext send path, dropping rich extras (sealed-only)"
1863                );
1864            }
1865            self.warn_plaintext_send(recipient);
1866            Ok(OutboundSendPreparation::Ready(content.to_string()))
1867        }
1868    }
1869
1870    // ========================================================================
1871    // PENDING / FLUSH
1872    // ========================================================================
1873
1874    /// Enforces [`MAX_MESSAGE_CONTENT_BYTES`] on application-supplied content.
1875    ///
1876    /// Enforced at the public send boundary rather than at transmit time,
1877    /// because a message that has to wait for MLS session establishment is
1878    /// queued — in memory and on disk — long before it ever reaches the
1879    /// transport's own size check. Without this cap a handful of very large
1880    /// sends could exhaust mobile memory and protocol-state storage while
1881    /// still satisfying the pending queue's entry-count limits.
1882    ///
1883    /// Applies to the plaintext the app hands us. MLS ciphertext, base64, and
1884    /// the JSON wire envelope expand it further, which is why the cap sits
1885    /// well under the transport's 1 MiB ceiling: everything accepted here can
1886    /// actually be delivered.
1887    ///
1888    /// Deliberately *not* applied to the group surface: a group send has no
1889    /// durable pre-session queue behind it (there is no per-recipient MLS
1890    /// session to wait on), so it is bounded by the transport alone — the
1891    /// documented "no send-side limit" behavior that
1892    /// `test_group_mls_send_large_content_no_send_side_limit` pins.
1893    pub(crate) fn check_content_size(content: &str) -> Result<()> {
1894        if content.len() > MAX_MESSAGE_CONTENT_BYTES {
1895            return Err(Error::InvalidArgument(format!(
1896                "Message content too large: {} bytes (max {}); use send_media for large payloads",
1897                content.len(),
1898                MAX_MESSAGE_CONTENT_BYTES
1899            )));
1900        }
1901        Ok(())
1902    }
1903
1904    #[allow(clippy::too_many_arguments)]
1905    pub(super) fn queue_message_for_session_establishment(
1906        &mut self,
1907        recipient: &str,
1908        content: &str,
1909        priority: MessagePriority,
1910        reply_to_msg_id: Option<MessageId>,
1911        forwarded_from: Option<ForwardInfo>,
1912        content_type: ContentType,
1913        media_metadata: Option<MediaMetadata>,
1914        rich: Option<RichSendExtras>,
1915        app_id: Option<&AppId>,
1916        provenance: Option<PendingProvenance>,
1917        reconciliation_reason: &'static str,
1918    ) -> Result<MessageId> {
1919        // Fresh sends mint an ID without ticking the Lamport clock — the
1920        // real tick happens when flush_pending_messages re-sends after the
1921        // session is established. A flush-time re-queue passes the id the
1922        // caller already holds so Deferred/Sent/Delivered stay correlatable,
1923        // and the timestamp it first entered the queue so the absolute
1924        // pending lifetime is not renewed by the round trip.
1925        let (message_id, first_queued_at) = match provenance {
1926            Some(provenance) => (provenance.message_id, provenance.first_queued_at),
1927            None => (MessageId::default(), None),
1928        };
1929
1930        debug!(
1931            recipient = %recipient,
1932            message_id = %message_id,
1933            "Message queued pending session establishment"
1934        );
1935        self.queue_pending_message_at(
1936            recipient,
1937            content,
1938            priority,
1939            message_id.clone(),
1940            reply_to_msg_id,
1941            forwarded_from,
1942            content_type,
1943            media_metadata,
1944            rich,
1945            app_id.map(|id| id.as_str().to_string()),
1946            first_queued_at.unwrap_or_else(Utc::now),
1947        );
1948        self.kick_pending_session_reconciliation(reconciliation_reason);
1949        if self.has_terminal_welcome_failure(recipient) {
1950            self.abort_pending_session_for_peer(
1951                recipient,
1952                crate::events::WelcomeReasonCode::RetryExhausted,
1953            );
1954            return Err(Error::Other(format!(
1955                "Welcome delivery failed for {}",
1956                recipient
1957            )));
1958        }
1959
1960        Ok(message_id)
1961    }
1962
1963    /// Queues a message with a specific message ID for later sending when
1964    /// session is established, starting a fresh pending lifetime.
1965    ///
1966    /// A re-queue must go through [`Self::queue_pending_message_at`] with the
1967    /// entry's original timestamp instead — which is why production code always
1968    /// calls that one directly (through
1969    /// `queue_message_for_session_establishment`, where the distinction is
1970    /// made) and this convenience exists only for tests.
1971    #[cfg(test)]
1972    #[allow(clippy::too_many_arguments)]
1973    pub(super) fn queue_pending_message(
1974        &mut self,
1975        recipient: &str,
1976        content: &str,
1977        priority: MessagePriority,
1978        message_id: MessageId,
1979        reply_to_msg: Option<MessageId>,
1980        forwarded_from: Option<ForwardInfo>,
1981        content_type: ContentType,
1982        media_metadata: Option<MediaMetadata>,
1983        rich: Option<RichSendExtras>,
1984    ) {
1985        self.queue_pending_message_at(
1986            recipient,
1987            content,
1988            priority,
1989            message_id,
1990            reply_to_msg,
1991            forwarded_from,
1992            content_type,
1993            media_metadata,
1994            rich,
1995            None,
1996            Utc::now(),
1997        );
1998    }
1999
2000    /// Queues a message with an explicit first-queued timestamp.
2001    ///
2002    /// `queued_at` is the instant the message *first* entered the pending
2003    /// queue, which is what `pending_message_max_lifetime_ms` is measured from.
2004    /// Re-queueing an entry (a flush that found the session still unavailable)
2005    /// must pass the original value: stamping `now()` there would hand the
2006    /// message a fresh window on every failed reconciliation, so a message
2007    /// could outlive an "absolute" lifetime indefinitely.
2008    ///
2009    /// Admission is bounded on two axes — entry count and serialized bytes,
2010    /// per peer and globally — because message content is
2011    /// application-supplied and a count alone bounds neither memory nor
2012    /// durable storage. Both axes evict oldest-first and settle each evicted
2013    /// message as failed.
2014    #[allow(clippy::too_many_arguments)]
2015    pub(super) fn queue_pending_message_at(
2016        &mut self,
2017        recipient: &str,
2018        content: &str,
2019        priority: MessagePriority,
2020        message_id: MessageId,
2021        reply_to_msg: Option<MessageId>,
2022        forwarded_from: Option<ForwardInfo>,
2023        content_type: ContentType,
2024        media_metadata: Option<MediaMetadata>,
2025        rich: Option<RichSendExtras>,
2026        app_id: Option<String>,
2027        queued_at: DateTime<Utc>,
2028    ) {
2029        let message_id_str = message_id.as_str().to_string();
2030        let mut pending = PendingMessage {
2031            content: content.to_string(),
2032            priority,
2033            message_id,
2034            reply_to_msg,
2035            forwarded_from,
2036            content_type,
2037            media_metadata,
2038            rich,
2039            app_id,
2040            queued_at,
2041            serialized_bytes: 0,
2042        };
2043        pending.measure();
2044        let incoming_bytes = pending.serialized_bytes;
2045
2046        let mut evicted = Vec::new();
2047
2048        while self
2049            .pending_encrypted_messages
2050            .get(recipient)
2051            .is_some_and(|messages| messages.len() >= MAX_PENDING_MESSAGES_PER_PEER)
2052        {
2053            if let Some(message) = self.evict_pending_message_at(recipient, 0) {
2054                evicted.push(message);
2055            } else {
2056                break;
2057            }
2058        }
2059
2060        // The running totals are decremented as entries are evicted rather
2061        // than recomputed each turn, so a queue at capacity does not make
2062        // admission quadratic.
2063        let mut peer_bytes = self.pending_message_bytes_for(recipient);
2064        while peer_bytes.saturating_add(incoming_bytes) > MAX_PENDING_MESSAGE_BYTES_PER_PEER {
2065            let Some(message) = self.evict_pending_message_at(recipient, 0) else {
2066                break;
2067            };
2068            peer_bytes = peer_bytes.saturating_sub(message.serialized_bytes);
2069            evicted.push(message);
2070        }
2071
2072        while self.total_pending_message_count() >= MAX_PENDING_MESSAGES_GLOBAL {
2073            let Some((_, message)) = self.evict_oldest_pending_message() else {
2074                break;
2075            };
2076            evicted.push(message);
2077        }
2078
2079        let mut global_bytes = self.total_pending_message_bytes();
2080        while global_bytes.saturating_add(incoming_bytes) > MAX_PENDING_MESSAGE_BYTES_GLOBAL {
2081            let Some((_, message)) = self.evict_oldest_pending_message() else {
2082                break;
2083            };
2084            global_bytes = global_bytes.saturating_sub(message.serialized_bytes);
2085            evicted.push(message);
2086        }
2087
2088        // Push to in-memory queue first, then persist (the in-memory queue
2089        // is the source of truth; storage is a crash-recovery backup).
2090        //
2091        // One record per message, so this writes the incoming entry and nothing
2092        // else — the queue it joins is already on disk, entry by entry, and the
2093        // entries the caps just evicted are deleted by id rather than rewritten
2094        // around. Which recipients were touched no longer matters.
2095        self.pending_encrypted_messages
2096            .entry(recipient.to_string())
2097            .or_default()
2098            .push(pending);
2099        self.note_pending_message_expiry(queued_at);
2100
2101        if let Some(queued) = self
2102            .pending_encrypted_messages
2103            .get(recipient)
2104            .and_then(|messages| messages.last())
2105        {
2106            self.persist_pending_message(recipient, queued);
2107        }
2108
2109        for message in evicted {
2110            self.delete_pending_message_from_storage(&message.message_id);
2111            self.emit_event(Event::message_failed(
2112                message.message_id,
2113                "Pending session queue capacity exceeded",
2114                0,
2115            ));
2116        }
2117
2118        debug!(recipient = %recipient, message_id = %message_id_str, "Queued message pending session establishment");
2119    }
2120
2121    pub(super) fn total_pending_message_count(&self) -> usize {
2122        self.pending_encrypted_messages.values().map(Vec::len).sum()
2123    }
2124
2125    /// Total serialized footprint of the pending-session queue across all
2126    /// peers. Summing precomputed per-entry sizes, so this stays a cheap
2127    /// integer walk rather than re-serializing the queue.
2128    pub(super) fn total_pending_message_bytes(&self) -> usize {
2129        self.pending_encrypted_messages
2130            .values()
2131            .flatten()
2132            .map(|message| message.serialized_bytes)
2133            .sum()
2134    }
2135
2136    /// Serialized footprint of one peer's pending-session queue.
2137    pub(super) fn pending_message_bytes_for(&self, recipient: &str) -> usize {
2138        self.pending_encrypted_messages
2139            .get(recipient)
2140            .map(|messages| {
2141                messages
2142                    .iter()
2143                    .map(|message| message.serialized_bytes)
2144                    .sum()
2145            })
2146            .unwrap_or(0)
2147    }
2148
2149    fn evict_pending_message_at(
2150        &mut self,
2151        recipient: &str,
2152        index: usize,
2153    ) -> Option<PendingMessage> {
2154        let (message, remove_recipient) = {
2155            let messages = self.pending_encrypted_messages.get_mut(recipient)?;
2156            if index >= messages.len() {
2157                return None;
2158            }
2159            let message = messages.remove(index);
2160            (message, messages.is_empty())
2161        };
2162        if remove_recipient {
2163            self.pending_encrypted_messages.remove(recipient);
2164        }
2165        Some(message)
2166    }
2167
2168    pub(super) fn evict_oldest_pending_message(&mut self) -> Option<(String, PendingMessage)> {
2169        let (recipient, index) = self
2170            .pending_encrypted_messages
2171            .iter()
2172            .flat_map(|(recipient, messages)| {
2173                messages
2174                    .iter()
2175                    .enumerate()
2176                    .map(move |(index, message)| (recipient, index, message))
2177            })
2178            .min_by(|left, right| {
2179                left.2
2180                    .queued_at
2181                    .cmp(&right.2.queued_at)
2182                    .then_with(|| left.2.message_id.as_str().cmp(&right.2.message_id.as_str()))
2183            })
2184            .map(|(recipient, index, _)| (recipient.clone(), index))?;
2185        let message = self.evict_pending_message_at(&recipient, index)?;
2186        Some((recipient, message))
2187    }
2188
2189    /// Drops a recipient's whole queue from memory *and* from storage, settling
2190    /// every dropped entry as `MessageFailed`.
2191    ///
2192    /// The storage half is a delete per entry rather than one whole-queue
2193    /// delete, which is the cost side of keying records by message id. Callers
2194    /// are the paths where every queued message for a peer becomes undeliverable
2195    /// at once — blocking, an aborted session.
2196    ///
2197    /// Settlement happens *here* rather than in the callers on purpose. The
2198    /// caller holds ids handed to the app by `send_message*` at queue time, so
2199    /// a silent drop leaves the app waiting forever on ids that will never
2200    /// resolve — the same contract the blocked-recipient branch of
2201    /// [`Self::flush_pending_messages`] and the pending-expiry pass already
2202    /// honour. Returning the messages for the caller to settle is what let all
2203    /// three call sites discard them instead, so there is nothing to discard
2204    /// any more.
2205    pub(super) fn drop_pending_queue_for_peer(
2206        &mut self,
2207        recipient: &str,
2208        reason: &'static str,
2209    ) -> usize {
2210        let messages = self
2211            .pending_encrypted_messages
2212            .remove(recipient)
2213            .unwrap_or_default();
2214        self.delete_pending_messages_from_storage(messages.iter().map(|m| &m.message_id));
2215        for message in &messages {
2216            self.emit_event(Event::message_failed(message.message_id.clone(), reason, 0));
2217        }
2218        self.recompute_next_pending_message_expiry();
2219        messages.len()
2220    }
2221
2222    fn pending_message_expiry(queued_at: DateTime<Utc>, lifetime_ms: u64) -> Option<DateTime<Utc>> {
2223        let lifetime_ms = i64::try_from(lifetime_ms).ok()?;
2224        queued_at.checked_add_signed(chrono::Duration::milliseconds(lifetime_ms))
2225    }
2226
2227    fn note_pending_message_expiry(&mut self, queued_at: DateTime<Utc>) {
2228        let Some(expiry) = Self::pending_message_expiry(
2229            queued_at,
2230            self.config
2231                .reliability
2232                .retry
2233                .pending_message_max_lifetime_ms,
2234        ) else {
2235            return;
2236        };
2237        self.next_pending_message_expiry = Some(
2238            self.next_pending_message_expiry
2239                .map_or(expiry, |current| current.min(expiry)),
2240        );
2241    }
2242
2243    pub(super) fn recompute_next_pending_message_expiry(&mut self) {
2244        let lifetime_ms = self
2245            .config
2246            .reliability
2247            .retry
2248            .pending_message_max_lifetime_ms;
2249        self.next_pending_message_expiry = self
2250            .pending_encrypted_messages
2251            .values()
2252            .flat_map(|messages| messages.iter())
2253            .filter_map(|message| Self::pending_message_expiry(message.queued_at, lifetime_ms))
2254            .min();
2255    }
2256
2257    /// Flushes pending messages for a recipient after session is established.
2258    ///
2259    /// Every message flushes through one unified path — prepare (re-making
2260    /// the seal-or-drop decision for rich extras against the recipient's
2261    /// *current* capability; the key package that confirmed this session may
2262    /// have advertised it), create, restore outer fields, then dispatch —
2263    /// and keeps the `message_id` the caller received from `send_message*`
2264    /// at queue time (no event fires on queueing — the returned id is the
2265    /// correlation anchor), so that id matches the eventual
2266    /// `MessageSent`/`MessageDelivered`/`MessageFailed`. A re-queue (session
2267    /// flapped back to not-ready) keeps the id too.
2268    pub(super) fn flush_pending_messages(&mut self, recipient: &str) -> Result<()> {
2269        if let Some(pending) = self.pending_encrypted_messages.remove(recipient) {
2270            // Blocking is bidirectional (we neither receive from nor send
2271            // to): a recipient blocked after messages were queued drops the
2272            // queue outright — retrying would fail with UserBlocked forever.
2273            if self.is_user_blocked(recipient) {
2274                info!(
2275                    recipient = %recipient,
2276                    count = pending.len(),
2277                    "Dropping pending messages for blocked recipient"
2278                );
2279                // The caller holds these ids (returned by `send_message*`
2280                // at queue time) — settle each one so the app isn't left
2281                // waiting on ids that will never resolve.
2282                for msg in &pending {
2283                    self.delete_pending_message_from_storage(&msg.message_id);
2284                    self.emit_event(Event::message_failed(
2285                        msg.message_id.clone(),
2286                        "Recipient blocked",
2287                        0,
2288                    ));
2289                }
2290                return Ok(());
2291            }
2292
2293            info!(recipient = %recipient, count = pending.len(), "Flushing pending messages");
2294            let original_order: Vec<String> =
2295                pending.iter().map(|m| m.message_id.as_str()).collect();
2296            let mut remaining = Vec::new();
2297            let now = Utc::now();
2298            let lifetime_ms = self
2299                .config
2300                .reliability
2301                .retry
2302                .pending_message_max_lifetime_ms;
2303
2304            for msg in pending {
2305                // The absolute pending lifetime is enforced here as well as in
2306                // the expiry pass, because that pass is bounded per tick
2307                // (`MAX_PENDING_EXPIRIES_PER_PASS`) — a restore can hand this
2308                // flush entries already past their deadline, and dispatching
2309                // one would settle it `MessageSent` after the lifetime promised
2310                // `message_failed`. Back into `remaining` instead: the entry
2311                // keeps its record, and the next expiry pass settles and
2312                // deletes it as a pair.
2313                if lifetime_expired(now, msg.queued_at, lifetime_ms) {
2314                    remaining.push(msg);
2315                    continue;
2316                }
2317
2318                // With stable ids the deduplicator now guards against a
2319                // double flush (e.g. a stale storage snapshot restored after
2320                // the message already went out): a dedup hit means this id
2321                // was dispatched before — drop it, don't retry forever.
2322                if self.deduplicator.is_duplicate(&msg.message_id) {
2323                    if self.deduplicator.is_exact() {
2324                        // Exact mode: the hit is authoritative — this id was
2325                        // already dispatched and (barring an internal error
2326                        // after the transport accepted it) settled then via
2327                        // MessageSent or MessageDeferred; re-dispatching
2328                        // would duplicate on the wire.
2329                        debug!(
2330                            message_id = %msg.message_id,
2331                            "Pending message already dispatched (dedup hit), dropping"
2332                        );
2333                    } else {
2334                        // Bloom mode: the hit may be a false positive for a
2335                        // message that never went out, and dispatching anyway
2336                        // would error-loop on the same filter. Settle loudly
2337                        // so the app can resend under a fresh id instead of
2338                        // losing the message silently.
2339                        warn!(
2340                            message_id = %msg.message_id,
2341                            "Pending message dropped on probabilistic dedup hit"
2342                        );
2343                        self.emit_event(Event::message_failed(
2344                            msg.message_id.clone(),
2345                            "Duplicate suppressed at flush (probabilistic dedup; possible false positive)",
2346                            0,
2347                        ));
2348                    }
2349                    // Leaves the queue for good either way, so its record goes
2350                    // with it. Only the entries that end up back in `remaining`
2351                    // below keep theirs — those were never deleted, and their
2352                    // contents have not changed, so re-queuing them costs no
2353                    // write at all.
2354                    self.delete_pending_message_from_storage(&msg.message_id);
2355                    continue;
2356                }
2357
2358                // Validated at admission and sealed at rest, so a failure
2359                // here means a corrupted record. Sending under the configured
2360                // id beats re-queueing an entry that can never flush.
2361                let flush_app_id = match Self::parse_send_app_id(msg.app_id.as_deref()) {
2362                    Ok(app_id) => app_id,
2363                    Err(e) => {
2364                        warn!(message_id = %msg.message_id, error = %e, "Pending message app id unreadable, using the configured id");
2365                        None
2366                    }
2367                };
2368
2369                let final_content = match self.prepare_outbound_content(
2370                    recipient,
2371                    &msg.content,
2372                    msg.priority,
2373                    msg.reply_to_msg.clone(),
2374                    msg.forwarded_from.clone(),
2375                    msg.content_type,
2376                    msg.media_metadata.clone(),
2377                    msg.rich.as_ref(),
2378                    flush_app_id.as_ref(),
2379                    Some(PendingProvenance::requeued(&msg)),
2380                    "flush_pending",
2381                ) {
2382                    Ok(OutboundSendPreparation::Ready(c)) => c,
2383                    Ok(OutboundSendPreparation::Queued(message_id)) => {
2384                        // Re-queued for later (session still not ready),
2385                        // keeping the original id.
2386                        debug!(message_id = %message_id, "Pending message re-queued");
2387                        continue;
2388                    }
2389                    Err(e) => {
2390                        warn!(message_id = %msg.message_id, error = %e, "Failed to prepare pending message");
2391                        remaining.push(msg);
2392                        continue;
2393                    }
2394                };
2395
2396                let message = match self.create_message_for_app(
2397                    recipient,
2398                    final_content,
2399                    Some(msg.priority),
2400                    msg.reply_to_msg.clone(),
2401                    flush_app_id,
2402                ) {
2403                    Ok(mut message) => {
2404                        // Restore outer fields: forward attribution and its
2405                        // media_metadata ride outer as the fallback for
2406                        // non-capable recipients (secrets stripped at the
2407                        // wire); when `msg.rich` sealed above, the receiver's
2408                        // sealed-body restore overwrites these wholesale.
2409                        // Plain sends store None for both — their rich
2410                        // copies are sealed or dropped by prepare above.
2411                        message.forwarded_from = msg.forwarded_from.clone();
2412                        message.content_type = msg.content_type;
2413                        message.media_metadata = msg.media_metadata.clone();
2414                        message.id = msg.message_id.clone();
2415                        message
2416                    }
2417                    Err(e) => {
2418                        warn!(message_id = %msg.message_id, error = %e, "Failed to create pending message");
2419                        remaining.push(msg);
2420                        continue;
2421                    }
2422                };
2423
2424                // Tier 2 provenance: a flushed pending message that sealed
2425                // carries its plaintext forward so later resends re-seal against
2426                // the peer's current session (see `send_message_with`).
2427                if message.content.starts_with(internal_prefixes::ENCRYPTED) {
2428                    self.stage_outbox_reseal(
2429                        &message.id,
2430                        recipient,
2431                        OutboxReseal {
2432                            content: msg.content.clone(),
2433                            priority: msg.priority,
2434                            reply_to_msg: msg.reply_to_msg.clone(),
2435                            forwarded_from: msg.forwarded_from.clone(),
2436                            content_type: msg.content_type,
2437                            media_metadata: msg.media_metadata.clone(),
2438                            rich: msg.rich.clone(),
2439                            seal_mark: None,
2440                        },
2441                    );
2442                }
2443
2444                match self.dispatch_prepared_message(message) {
2445                    Ok(id) => {
2446                        debug!(message_id = %id, "Sent pending message");
2447                        // Handed off to the outbox, which persists its own
2448                        // record under the same id — so the pending copy has to
2449                        // go, or a restart would restore and re-flush it.
2450                        self.delete_pending_message_from_storage(&id);
2451                    }
2452                    Err(e) => {
2453                        warn!(message_id = %msg.message_id, error = %e, "Failed to send pending message");
2454                        remaining.push(msg);
2455                    }
2456                }
2457            }
2458
2459            // Messages that re-queued above re-inserted themselves into the
2460            // map (and rewrote their own record) — merge failures in without
2461            // clobbering the entry. Nothing to clear when neither survives: each
2462            // entry that left the queue already took its own record with it.
2463            if remaining.is_empty() {
2464                return Ok(());
2465            }
2466            if self.has_terminal_welcome_failure(recipient) {
2467                // A prepare failure above came from a terminal Welcome
2468                // failure: abort_pending_session_for_peer already cleared
2469                // the queue and its storage and settled the peer with
2470                // secure_session_failed — re-inserting `remaining` would
2471                // resurrect messages the abort just settled, on every
2472                // future flush, forever.
2473                //
2474                // The abort could only delete the records of entries still in
2475                // the map, and this flush took the queue out of it at the top,
2476                // so these records are still on disk and have to go here or the
2477                // next launch restores exactly what the abort settled.
2478                debug!(
2479                    recipient = %recipient,
2480                    count = remaining.len(),
2481                    "Dropping flush failures for aborted pending session"
2482                );
2483                for msg in &remaining {
2484                    self.delete_pending_message_from_storage(&msg.message_id);
2485                }
2486            } else {
2487                // Every other path into `pending_encrypted_messages` notes the
2488                // entry's deadline. This one is a *re*-insertion of entries the
2489                // queue already held, so today the cached minimum is still
2490                // correct without it — but only because nothing recomputes that
2491                // cache while a flush is in progress. Noting them keeps
2492                // "`next_pending_message_expiry` is never later than the true
2493                // minimum" an invariant of the code rather than of the call
2494                // graph: if it ever broke, these entries would stop being
2495                // scanned and would never expire, silently, on the one queue the
2496                // absolute pending lifetime exists to bound.
2497                let merged_deadlines: Vec<DateTime<Utc>> =
2498                    remaining.iter().map(|m| m.queued_at).collect();
2499                let queue = self
2500                    .pending_encrypted_messages
2501                    .entry(recipient.to_string())
2502                    .or_default();
2503                queue.extend(remaining);
2504                // A failure that preceded a mid-flush re-queue would land
2505                // behind it — restore the original queue order (stable
2506                // sort; every id here came from this flush's queue).
2507                queue.sort_by_key(|m| {
2508                    original_order
2509                        .iter()
2510                        .position(|id| *id == m.message_id.as_str())
2511                        .unwrap_or(usize::MAX)
2512                });
2513                for queued_at in merged_deadlines {
2514                    self.note_pending_message_expiry(queued_at);
2515                }
2516                // No write: these entries never left the queue durably. Their
2517                // records were written at enqueue, were not deleted above, and
2518                // their contents have not changed — only their position in the
2519                // in-memory queue, which the restore ordering rebuilds from
2520                // `queued_at` rather than from storage order.
2521            }
2522        }
2523        Ok(())
2524    }
2525
2526    pub(super) fn flush_restored_confirmed_pending_messages(&mut self) {
2527        let recipients: Vec<String> = self.pending_encrypted_messages.keys().cloned().collect();
2528
2529        for recipient in recipients {
2530            match self.is_session_confirmed(&recipient) {
2531                Ok(true) => {
2532                    if let Err(err) = self.flush_pending_messages(&recipient) {
2533                        warn!(
2534                            recipient = %recipient,
2535                            error = %err,
2536                            "Failed to flush restored pending messages for confirmed session"
2537                        );
2538                    }
2539                }
2540                Ok(false) => {}
2541                Err(err) => {
2542                    warn!(
2543                        recipient = %recipient,
2544                        error = %err,
2545                        "Failed to read session confirmation state while restoring pending messages"
2546                    );
2547                }
2548            }
2549        }
2550    }
2551
2552    // ========================================================================
2553    // MEDIA
2554    // ========================================================================
2555
2556    /// Sends a media attachment (image, video, audio, file, etc.) to a recipient.
2557    ///
2558    /// The file data is chunked and each chunk is sent as an individual message
2559    /// with `content_type: FileChunk`. The first chunk carries the full
2560    /// `MediaMetadata` so the receiver can display a preview before all chunks
2561    /// arrive. Individual chunk messages require ACKs and participate in retry
2562    /// logic so delivery is tracked and recoverable per chunk. `MediaSent` is
2563    /// emitted only after all chunks are ACKed.
2564    ///
2565    /// # Encryption
2566    ///
2567    /// With auto-encryption active (encryption enabled and MLS initialized),
2568    /// chunk bytes — and the chunk-0 media metadata and original content type —
2569    /// travel inside MLS ciphertext, using the same session the text path uses.
2570    /// The session must already be confirmed: media is never queued pending
2571    /// establishment and never falls back to plaintext. When the session is
2572    /// not ready this returns [`Error::SessionNotReady`] (after kicking
2573    /// establishment if a key package is available); retry after the
2574    /// `secure_session_established` event. When encryption is required but MLS
2575    /// is not initialized this returns [`Error::EncryptFailed`]. Plaintext
2576    /// chunks are sent only when auto-encryption is inactive AND
2577    /// `require_encryption` was explicitly set to `false` (it defaults to
2578    /// `true`): the explicit encryption opt-out, or encryption enabled but
2579    /// MLS never initialized (matching the text path). Every plaintext
2580    /// transfer emits a
2581    /// [`crate::events::SecurityWarningCode::PlaintextSend`] warning, once
2582    /// per peer.
2583    ///
2584    /// Returns a `file_id` that can be used to track progress or cancel.
2585    pub fn send_media(
2586        &mut self,
2587        recipient: impl Into<String>,
2588        file_data: Vec<u8>,
2589        file_name: impl Into<String>,
2590        content_type: ContentType,
2591        media_metadata: Option<MediaMetadata>,
2592    ) -> Result<String> {
2593        self.send_media_with(
2594            recipient,
2595            file_data,
2596            file_name,
2597            content_type,
2598            MediaSendOptions {
2599                media_metadata,
2600                ..Default::default()
2601            },
2602        )
2603    }
2604
2605    /// Sends a media attachment with rich options: the chunk-0 media
2606    /// metadata (as on [`Self::send_media`]), plus caption, reply threading,
2607    /// quoted-reply context, forward attribution, and an optional
2608    /// caller-supplied `file_id`.
2609    ///
2610    /// The rich fields only ever travel inside the MLS-sealed chunk-0
2611    /// plaintext (under the v2 media envelope), and only toward recipients
2612    /// whose key package advertised `rich_versions` support (gated by
2613    /// `EncryptionConfig::rich_payload_enabled`). Toward anyone else —
2614    /// including every plaintext (encryption opt-out) transfer — they are
2615    /// silently dropped, never sent cleartext.
2616    ///
2617    /// Returns `InvalidArgument` for an unparsable `reply_to_msg`, rich
2618    /// extras whose serialized size exceeds 32 KiB, or a `file_id` that is
2619    /// empty, over the wire field bound, or already carried by an active
2620    /// outbound transfer. Encryption preconditions match
2621    /// [`Self::send_media`].
2622    pub fn send_media_with(
2623        &mut self,
2624        recipient: impl Into<String>,
2625        file_data: Vec<u8>,
2626        file_name: impl Into<String>,
2627        content_type: ContentType,
2628        options: MediaSendOptions,
2629    ) -> Result<String> {
2630        // The public surface never carries a data purpose. Every transfer
2631        // reachable from an application is a transfer for its user, and the
2632        // one argument that could change that is not in `MediaSendOptions`
2633        // and must never be added to it: a caller able to mark a send as
2634        // data-purposed could feed bytes of its choosing to a peer's
2635        // document engine while that peer's user saw nothing arrive.
2636        self.send_media_inner(recipient, file_data, file_name, content_type, options, None)
2637    }
2638
2639    /// The body of [`Self::send_media_with`], plus the one argument the
2640    /// public surface may not supply.
2641    ///
2642    /// `data_purpose` marks a transfer as belonging to the replicated-
2643    /// document layer. It is `pub(crate)` and reached only from the data
2644    /// layer's own send seams.
2645    #[cfg_attr(not(feature = "data"), allow(dead_code))]
2646    pub(crate) fn send_media_inner(
2647        &mut self,
2648        recipient: impl Into<String>,
2649        file_data: Vec<u8>,
2650        file_name: impl Into<String>,
2651        content_type: ContentType,
2652        options: MediaSendOptions,
2653        data_purpose: Option<crate::media_envelope::DataPurpose>,
2654    ) -> Result<String> {
2655        {
2656            let state = lock_shared_state(&self.shared_state)?;
2657            if state.state != ProtocolState::Running {
2658                return Err(Error::NotStarted);
2659            }
2660        }
2661
2662        let recipient_str: String = recipient.into();
2663        let file_name_str: String = file_name.into();
2664        let media_metadata = options.media_metadata;
2665        Self::validate_outbound_recipient(&recipient_str)?;
2666        // Before any transfer state exists, like the recipient check.
2667        let app_id = Self::parse_send_app_id(options.app_id.as_deref())?;
2668
2669        // Validate the reply id like send_message_with does — a malformed id
2670        // fails the call instead of riding sealed to the receiver.
2671        if let Some(reply_to) = options.reply_to_msg.as_deref() {
2672            MessageId::from_str(reply_to)
2673                .map_err(|e| Error::InvalidArgument(format!("Invalid reply_to_msg: {}", e)))?;
2674        }
2675
2676        let rich_extras = MediaRichExtras {
2677            caption: options.caption,
2678            reply_to_msg: options.reply_to_msg,
2679            reply_context: options.reply_context,
2680            forward_info: options.forward_info,
2681        };
2682        // Same boundary cap as send_message_with: an oversized quote or
2683        // caption would inflate the sealed chunk-0 plaintext into heavy
2684        // fragmentation. Enforced before the capability gate so the caller
2685        // hears about it even when the extras would be dropped.
2686        if rich_extras.is_any() {
2687            let extras_len = serde_json::to_vec(&rich_extras)
2688                .map_err(|e| Error::Serialization(e.to_string()))?
2689                .len();
2690            if extras_len > MAX_RICH_EXTRAS_BYTES {
2691                return Err(Error::InvalidArgument(format!(
2692                    "Rich extras too large: {} bytes serialized (max {})",
2693                    extras_len, MAX_RICH_EXTRAS_BYTES
2694                )));
2695            }
2696        }
2697
2698        if let Some(file_id) = options.file_id.as_deref() {
2699            if file_id.is_empty() || file_id.len() > FileChunk::MAX_STRING_FIELD_LEN {
2700                return Err(Error::InvalidArgument(format!(
2701                    "file_id length must be 1..={} bytes",
2702                    FileChunk::MAX_STRING_FIELD_LEN
2703                )));
2704            }
2705            if self.outbound_media_transfers.contains_key(file_id) {
2706                return Err(Error::InvalidArgument(format!(
2707                    "file_id {} already has an active outbound transfer",
2708                    file_id
2709                )));
2710            }
2711            // A restored descriptor under this file_id means this send is
2712            // the app answering MediaResendRequired: it must target the
2713            // original recipient (a redirect would consume the descriptor
2714            // and orphan the interrupted transfer's resend forever) and
2715            // re-supply the original bytes — the receiver may already hold
2716            // chunks of the interrupted attempt, and a silent content swap
2717            // under the same id would fail its integrity check at best.
2718            if let Some(descriptor) = self.restored_media_descriptors.get(file_id) {
2719                if descriptor.recipient != recipient_str {
2720                    return Err(Error::InvalidArgument(format!(
2721                        "file_id {} belongs to an interrupted transfer to a different recipient",
2722                        file_id
2723                    )));
2724                }
2725                use sha2::{Digest, Sha256};
2726                let resupplied_checksum = format!("{:x}", Sha256::digest(&file_data));
2727                if resupplied_checksum != descriptor.file_checksum {
2728                    return Err(Error::InvalidArgument(format!(
2729                        "file_id {} resend bytes do not match the interrupted transfer's checksum",
2730                        file_id
2731                    )));
2732                }
2733            }
2734        }
2735
2736        // Prevent sending media to blocked users. Blocking is bidirectional:
2737        // we neither receive from nor send to a blocked peer.
2738        if self.is_user_blocked(&recipient_str) {
2739            return Err(Error::UserBlocked(recipient_str));
2740        }
2741
2742        // A data-purposed transfer goes only to a peer that said it routes
2743        // one into its document layer. Toward anybody else the bytes would
2744        // be handed to a person as a downloaded file, so this refuses rather
2745        // than degrading: there is no useful weaker form of this send.
2746        if data_purpose.is_some() && !self.data_media_active(&recipient_str) {
2747            return Err(Error::InvalidArgument(format!(
2748                "peer {recipient_str} did not advertise attachment carriage"
2749            )));
2750        }
2751
2752        // And only where it can be sealed. The marking exists nowhere but
2753        // inside the encrypted chunk-0 plaintext, so a transfer leaving by
2754        // the plaintext opt-out path arrives stripped of it: the receiver
2755        // has no way to know what the bytes are and hands its user a
2756        // downloaded file named by a hash, which is the outcome the whole
2757        // marking exists to prevent. Refused rather than degraded, for the
2758        // same reason the capability gate above refuses.
2759        if data_purpose.is_some() && !self.should_auto_encrypt() {
2760            return Err(Error::EncryptFailed(
2761                "a document-layer transfer cannot be sent unsealed: its marking travels \
2762                 inside the encrypted chunk, and a receiver would surface the bytes to its \
2763                 user as an ordinary file"
2764                    .to_string(),
2765            ));
2766        }
2767
2768        // SEC-H1: media rides the same MLS session machinery as text. With
2769        // auto-encryption active the session must already be confirmed —
2770        // files are too large for the pending-message queue, so media is
2771        // never queued pending establishment and never falls back to
2772        // plaintext. Callers should retry after `secure_session_established`.
2773        if self.should_auto_encrypt() {
2774            if !self.is_session_confirmed(&recipient_str)? {
2775                // Kick session establishment exactly like the text path
2776                // (import a stored key package, create the session, send the
2777                // Welcome), then report not-ready: the session cannot be
2778                // confirmed synchronously, so callers retry after
2779                // `secure_session_established`.
2780                if let Some(mls) = self.mls_manager.clone() {
2781                    // A document-layer transfer is the SDK's own: the
2782                    // Welcome it opens must not alert.
2783                    self.ensure_session_establishment(
2784                        &mls,
2785                        &recipient_str,
2786                        data_purpose.is_none(),
2787                    )?;
2788                }
2789                return Err(Error::SessionNotReady(
2790                    self.establishment_state(&recipient_str)?,
2791                ));
2792            }
2793
2794            // Encrypted chunks share the recipient's session ratchet with
2795            // text, and the receiver keeps out-of-order message keys for a
2796            // bounded number of generations — cap concurrent transfers so
2797            // the combined in-flight windows cannot push a delayed chunk
2798            // beyond that tolerance and permanently stall it.
2799            use crate::constants::{
2800                MAX_CONCURRENT_INTERNAL_MEDIA_TRANSFERS_PER_PEER,
2801                MAX_CONCURRENT_MEDIA_TRANSFERS_PER_PEER,
2802            };
2803            let active_transfers = self
2804                .outbound_media_transfers
2805                .values()
2806                .filter(|transfer| transfer.recipient == recipient_str)
2807                .count();
2808            if active_transfers >= MAX_CONCURRENT_MEDIA_TRANSFERS_PER_PEER {
2809                return Err(Error::MediaTransferLimit(recipient_str));
2810            }
2811
2812            // An SDK-internal transfer takes at most one of those slots.
2813            // The cap above bounds the ratchet gap and so cannot be raised
2814            // for our own traffic; what is left is to leave the application
2815            // a slot it can account for. The in-flight dedup stops a second
2816            // copy of one transfer, not two different ones, and two
2817            // different ones are ordinary here: a document snapshot and an
2818            // answered blob request are separate errands to the same peer.
2819            // Filling both slots with them fails the app's own send with a
2820            // limit whose cause it cannot see, because what holds the slots
2821            // is invisible to it by design.
2822            if data_purpose.is_some() {
2823                let internal_transfers = self
2824                    .outbound_media_transfers
2825                    .values()
2826                    .filter(|transfer| {
2827                        transfer.recipient == recipient_str && transfer.data_purpose.is_some()
2828                    })
2829                    .count();
2830                if internal_transfers >= MAX_CONCURRENT_INTERNAL_MEDIA_TRANSFERS_PER_PEER {
2831                    return Err(Error::MediaTransferLimit(recipient_str));
2832                }
2833            }
2834        } else if self.config.encryption.require_encryption {
2835            return Err(Error::EncryptFailed(
2836                "MLS encryption is required (the default) but MLS is not initialized — \
2837                 call initialize_mls() with secure and protocol-state storage, or explicitly opt out with \
2838                 require_encryption=false to send plaintext media"
2839                    .to_string(),
2840            ));
2841        } else {
2842            // Explicit opt-out: the whole transfer leaves as legacy plaintext
2843            // chunks. Warn once per peer, not per chunk.
2844            self.warn_plaintext_send(&recipient_str);
2845        }
2846
2847        // Rich extras seal only toward recipients that advertised the rich
2848        // payload, and only on the encrypted path; everyone else — including
2849        // every plaintext (opt-out) transfer — gets the plain transfer.
2850        // Dropped here, never sent cleartext.
2851        let rich_extras = if !rich_extras.is_any() {
2852            None
2853        } else if self.should_auto_encrypt() && self.rich_seal_active(&recipient_str) {
2854            Some(rich_extras)
2855        } else {
2856            debug!(
2857                recipient = %recipient_str,
2858                "Dropping rich media extras: recipient did not advertise sealed rich payload support"
2859            );
2860            None
2861        };
2862
2863        let file_id = options
2864            .file_id
2865            .unwrap_or_else(|| format!("file_{}", MessageId::new().as_str()));
2866        let pinned_transport = self.select_media_transport(&recipient_str)?;
2867
2868        let (chunk_size, window_size) = match pinned_transport {
2869            TransportType::BLE => {
2870                use crate::constants::{CHUNK_SIZE_BLE, MEDIA_WINDOW_SIZE_BLE};
2871                (CHUNK_SIZE_BLE, MEDIA_WINDOW_SIZE_BLE)
2872            }
2873            TransportType::Internet => {
2874                use crate::constants::{CHUNK_SIZE_INTERNET, MEDIA_WINDOW_SIZE_INTERNET};
2875                (CHUNK_SIZE_INTERNET, MEDIA_WINDOW_SIZE_INTERNET)
2876            }
2877            TransportType::WiFiDirect => {
2878                use crate::constants::{DEFAULT_CHUNK_SIZE, DEFAULT_MEDIA_WINDOW_SIZE};
2879                (DEFAULT_CHUNK_SIZE, DEFAULT_MEDIA_WINDOW_SIZE)
2880            }
2881            TransportType::Reticulum => {
2882                // Reticulum is low-bandwidth (LoRa); use BLE-like chunk sizes.
2883                use crate::constants::{CHUNK_SIZE_BLE, MEDIA_WINDOW_SIZE_BLE};
2884                (CHUNK_SIZE_BLE, MEDIA_WINDOW_SIZE_BLE)
2885            }
2886            TransportType::Nostr => {
2887                // Nostr relays have decent bandwidth; use Internet-like chunk sizes.
2888                use crate::constants::{CHUNK_SIZE_INTERNET, MEDIA_WINDOW_SIZE_INTERNET};
2889                (CHUNK_SIZE_INTERNET, MEDIA_WINDOW_SIZE_INTERNET)
2890            }
2891        };
2892        let chunks = self.file_transfer_manager.chunk_file(
2893            file_id.clone(),
2894            file_name_str,
2895            file_data,
2896            Some(chunk_size),
2897        )?;
2898
2899        let total_chunks = chunks.len() as u32;
2900        self.outbound_media_transfers.insert(
2901            file_id.clone(),
2902            OutboundMediaTransfer {
2903                content_type,
2904                recipient: recipient_str.clone(),
2905                pinned_transport,
2906                total_chunks,
2907                delivered_chunks: HashSet::new(),
2908                last_updated_at: Instant::now(),
2909                media_metadata: media_metadata.clone(),
2910                rich_extras: rich_extras.clone(),
2911                data_purpose: data_purpose.clone(),
2912                app_id: app_id.clone(),
2913            },
2914        );
2915
2916        // Persist the crash-recovery descriptor (no chunk bytes) so a
2917        // restart mid-transfer can tell the app to re-initiate via
2918        // MediaResendRequired. A same-file_id resend consumes the restored
2919        // copy here — its checksum was validated at the boundary above.
2920        if let Some(first_chunk) = chunks.first() {
2921            let descriptor = MediaTransferDescriptor {
2922                file_id: file_id.clone(),
2923                recipient: recipient_str.clone(),
2924                file_name: first_chunk.file_name.clone(),
2925                file_size: first_chunk.file_size,
2926                file_checksum: first_chunk.file_checksum.clone(),
2927                content_type,
2928                queued_at: Utc::now(),
2929                data_purpose: data_purpose.clone(),
2930                app_id: app_id.as_ref().map(|id| id.as_str().to_string()),
2931            };
2932            self.restored_media_descriptors.remove(&file_id);
2933            self.persist_media_descriptor(&descriptor);
2934        }
2935
2936        let mut window_state = OutboundTransferState::new(chunks, window_size);
2937        let initial_batch = window_state.next_chunks_to_send();
2938        self.outbound_media_windows
2939            .insert(file_id.clone(), window_state);
2940
2941        // A data-purposed transfer is invisible to the application on this
2942        // side too. The receiver's user must not be shown a download they did
2943        // not start; the sender's must not be shown an upload they did not
2944        // make, and `MediaSent` for one would put a file in a conversation.
2945        if data_purpose.is_none() {
2946            let state = lock_shared_state(&self.shared_state)?;
2947            state.emit_event(Event::file_progress(file_id.clone(), 0, total_chunks));
2948            drop(state);
2949        }
2950
2951        self.send_media_chunk_batch(
2952            &file_id,
2953            initial_batch,
2954            &recipient_str,
2955            pinned_transport,
2956            content_type,
2957            media_metadata.as_ref(),
2958            rich_extras.as_ref(),
2959            data_purpose.as_ref(),
2960            app_id.as_ref(),
2961        )?;
2962
2963        Ok(file_id)
2964    }
2965
2966    /// Sends a batch of file chunks, wiring each into the outbox and media tracking.
2967    #[allow(clippy::too_many_arguments)]
2968    pub(super) fn send_media_chunk_batch(
2969        &mut self,
2970        file_id: &str,
2971        chunks: Vec<FileChunk>,
2972        recipient: &str,
2973        pinned_transport: TransportType,
2974        content_type: ContentType,
2975        media_metadata: Option<&MediaMetadata>,
2976        rich_extras: Option<&MediaRichExtras>,
2977        data_purpose: Option<&crate::media_envelope::DataPurpose>,
2978        app_id: Option<&AppId>,
2979    ) -> Result<()> {
2980        for chunk in chunks {
2981            let chunk_index = chunk.chunk_index;
2982
2983            let meta_for_chunk = if chunk_index == 0 {
2984                media_metadata.cloned()
2985            } else {
2986                None
2987            };
2988
2989            // SEC-H1: with auto-encryption active, chunk bytes AND the chunk-0
2990            // metadata (file name, preview thumbnail, original content type)
2991            // travel inside the MLS ciphertext — the wire Message carries none
2992            // of them. The plaintext fields are only populated when
2993            // auto-encryption is inactive. Rich extras (caption, reply,
2994            // forward) exist only on this sealed path, chunk 0 only; their
2995            // presence bumps the envelope to v2 so a pre-rich receiver
2996            // rejects cleanly instead of misparsing.
2997            let (binary_payload, wire_metadata, wire_original_ct) = if self.should_auto_encrypt() {
2998                let inner = MediaChunkPlaintext {
2999                    chunk_bytes: chunk.to_bytes(),
3000                    media_metadata: meta_for_chunk,
3001                    original_content_type: (chunk_index == 0).then_some(content_type),
3002                    rich_extras: if chunk_index == 0 {
3003                        rich_extras.cloned()
3004                    } else {
3005                        None
3006                    },
3007                    data_purpose: if chunk_index == 0 {
3008                        data_purpose.cloned()
3009                    } else {
3010                        None
3011                    },
3012                };
3013                let envelope_version = inner.envelope_version();
3014                let sealed = inner
3015                    .encode()
3016                    .map_err(Error::Serialization)
3017                    .and_then(|plaintext| {
3018                        self.encrypt_bytes_for_recipient_strict(recipient, &plaintext)
3019                    });
3020                let encrypted = match sealed {
3021                    Ok(encrypted) => encrypted,
3022                    Err(err) => {
3023                        // The chunks this batch popped from the window are
3024                        // marked in-flight with no outbox entry — nothing
3025                        // would ever ACK or retry them, so the transfer can
3026                        // only wedge while holding a per-peer transfer slot.
3027                        // Encryption failure is not per-chunk transient:
3028                        // abort the whole transfer loudly.
3029                        self.abort_outbound_media_transfer(file_id, "chunk encryption failed");
3030                        return Err(err);
3031                    }
3032                };
3033                (
3034                    encode_media_envelope(&encrypted, envelope_version),
3035                    None,
3036                    false,
3037                )
3038            } else {
3039                (chunk.to_bytes(), meta_for_chunk, chunk_index == 0)
3040            };
3041
3042            let mut message = self.create_media_message(
3043                recipient,
3044                String::new(),
3045                ContentType::FileChunk,
3046                wire_metadata,
3047                app_id.cloned(),
3048            )?;
3049            message.binary_content = Some(binary_payload);
3050
3051            if wire_original_ct {
3052                use crate::constants::ORIGINAL_CONTENT_TYPE_KEY;
3053                message.metadata.insert(
3054                    ORIGINAL_CONTENT_TYPE_KEY.to_string(),
3055                    content_type.to_string(),
3056                );
3057            }
3058            // A document-layer transfer is the SDK's own traffic, like the
3059            // `__DATA_V1__` frames beside it. The purpose rides sealed in
3060            // chunk 0 only, so every chunk is marked here, where it is known.
3061            if data_purpose.is_some() {
3062                message
3063                    .metadata
3064                    .insert(crate::constants::NO_ALERT_KEY.to_string(), "1".to_string());
3065            }
3066            self.outbound_media_chunks
3067                .insert(message.id.clone(), (file_id.to_string(), chunk_index));
3068
3069            let previous_transport = self.transport_manager.current_transport();
3070            // A chunk held for a slot is sent by the retry queue, pinned to
3071            // the same carrier; the transfer's window counts it as in flight.
3072            let send_result = if self.defer_for_ack_capacity(&message).is_some() {
3073                None
3074            } else {
3075                Some(
3076                    self.transport_manager
3077                        .send_via_transport(&message, pinned_transport),
3078                )
3079            };
3080            let current_transport = Some(pinned_transport);
3081
3082            match send_result {
3083                None => {}
3084                Some(Ok(())) => {
3085                    self.handle_send_success(&message, current_transport)?;
3086                    self.emit_transport_switch_event(previous_transport, current_transport)?;
3087                }
3088                Some(Err(err)) => {
3089                    let _ = self
3090                        .handle_send_failure(&message, current_transport.or(previous_transport))?;
3091                    // send_via_transport does not record retry failures internally.
3092                    self.transport_manager
3093                        .record_retry_failure(pinned_transport);
3094                    warn!(
3095                        file_id = %file_id,
3096                        chunk_index = chunk_index,
3097                        transport = ?pinned_transport,
3098                        error = %err,
3099                        "File chunk send failed, message deferred"
3100                    );
3101                }
3102            }
3103            if !self.outbound_media_transfers.contains_key(file_id) {
3104                return Err(Error::Other(format!(
3105                    "Media transfer {} could not be scheduled for reliable delivery",
3106                    file_id
3107                )));
3108            }
3109        }
3110        Ok(())
3111    }
3112
3113    /// Pumps all active windowed media transfers, sending the next batch of
3114    /// chunks for any transfer whose window has capacity (previous chunks ACKed).
3115    /// Should be called from the periodic tick/poll loop.
3116    pub(super) fn pump_media_transfers(&mut self) {
3117        let file_ids: Vec<String> = self.outbound_media_windows.keys().cloned().collect();
3118
3119        for file_id in file_ids {
3120            let transfer = match self.outbound_media_transfers.get(&file_id) {
3121                Some(t) => t.clone(),
3122                None => {
3123                    self.outbound_media_windows.remove(&file_id);
3124                    continue;
3125                }
3126            };
3127
3128            let window = match self.outbound_media_windows.get_mut(&file_id) {
3129                Some(w) => w,
3130                None => continue,
3131            };
3132
3133            if !window.has_capacity() {
3134                continue;
3135            }
3136
3137            let batch = window.next_chunks_to_send();
3138            if batch.is_empty() {
3139                continue;
3140            }
3141
3142            if let Err(err) = self.send_media_chunk_batch(
3143                &file_id,
3144                batch,
3145                &transfer.recipient,
3146                transfer.pinned_transport,
3147                transfer.content_type,
3148                transfer.media_metadata.as_ref(),
3149                transfer.rich_extras.as_ref(),
3150                transfer.data_purpose.as_ref(),
3151                transfer.app_id.as_ref(),
3152            ) {
3153                warn!(
3154                    file_id = %file_id,
3155                    error = %err,
3156                    "Failed to pump media transfer chunks"
3157                );
3158            }
3159        }
3160    }
3161
3162    /// Creates a message carrying media content (file chunks, etc.).
3163    ///
3164    /// Like `create_message` but sets `content_type`, `media_metadata`, marks
3165    /// the message as internet-preferred via metadata, and requires per-chunk ACKs.
3166    pub(super) fn create_media_message(
3167        &mut self,
3168        recipient: &str,
3169        content: impl Into<String>,
3170        content_type: ContentType,
3171        media_metadata: Option<MediaMetadata>,
3172        app_id: Option<AppId>,
3173    ) -> Result<Message> {
3174        use crate::constants::{TRANSPORT_PREFERENCE_INTERNET, TRANSPORT_PREFERENCE_KEY};
3175
3176        let sender = UserId::new(&self.local_id)?;
3177        let recipient = UserId::new(recipient)?;
3178        let app_id = match app_id {
3179            Some(app_id) => app_id,
3180            None => AppId::new(&self.config.app_id)?,
3181        };
3182
3183        let clock_value = self.lamport_clock.tick();
3184        self.persist_lamport_clock();
3185
3186        let mut builder = Message::builder(sender, recipient, app_id)
3187            .content(content)
3188            .content_type(content_type)
3189            .priority(MessagePriority::Medium)
3190            .ttl(TTL::new(self.config.initial_ttl)?)
3191            .lamport_clock(clock_value)
3192            .metadata(TRANSPORT_PREFERENCE_KEY, TRANSPORT_PREFERENCE_INTERNET)
3193            .requires_ack(true);
3194
3195        if let Some(meta) = media_metadata {
3196            builder = builder.media_metadata(meta);
3197        }
3198
3199        Ok(builder.build())
3200    }
3201
3202    // ========================================================================
3203    // OUTBOX
3204    // ========================================================================
3205
3206    /// Whether a full outbox may give up `entry` to make room.
3207    ///
3208    /// Never a message awaiting its acknowledgement: it is on the wire, and
3209    /// under load on a live link evicting it turned every new send into the
3210    /// failure of a frame already sent, whose acknowledgement then settled
3211    /// nothing.
3212    ///
3213    /// A message the per-peer window holds has not been sent, so it may go,
3214    /// but last in its recipient's line ([`Self::eviction_order_key`]). It was
3215    /// once never evictable on a live link, and then one slow peer whose held
3216    /// line filled the outbox made every send to every other recipient a
3217    /// refusal.
3218    fn outbox_entry_evictable(&self, id: &MessageId, entry: &OutboxEntry) -> bool {
3219        !(entry.message.requires_ack && self.ack_manager.is_waiting_for_ack(id))
3220    }
3221
3222    /// Each message the per-peer window holds for a recipient this device
3223    /// has a live mesh link to, with its place in that recipient's line
3224    /// (0 is the front, the next to be sent).
3225    fn held_line_positions(&self) -> HashMap<MessageId, usize> {
3226        self.window_held
3227            .iter()
3228            .filter(|(recipient, _)| {
3229                self.transport_manager
3230                    .mesh_transport_for(recipient)
3231                    .is_some()
3232            })
3233            .flat_map(|(_, held)| held.iter().cloned().enumerate().map(|(at, id)| (id, at)))
3234            .collect()
3235    }
3236
3237    /// Orders the evictable entries of one recipient: lower goes first.
3238    ///
3239    /// Held messages for a live peer go first, from the back of the line,
3240    /// so what survives is the front, the part about to be sent, and the
3241    /// conversation stays contiguous. Their place in the line decides, not a
3242    /// timestamp: a burst is held within one millisecond. Everything else
3243    /// goes oldest first: a message to an unreachable peer that has waited
3244    /// longest is the least likely to land.
3245    fn eviction_order_key(held: &HashMap<MessageId, usize>, entry: &OutboxEntry) -> (u8, i64) {
3246        match held.get(&entry.message.id) {
3247            Some(&at) => (0, -(at as i64)),
3248            None => (1, entry.first_sent_at.timestamp_millis()),
3249        }
3250    }
3251
3252    /// Evicts one entry from a full outbox, choosing from the recipient with
3253    /// the largest backlog, oldest first, and only entries
3254    /// [`Self::outbox_entry_evictable`] allows. With `newcomer` set, an
3255    /// app send is asking: only a recipient with a strictly larger backlog
3256    /// than the newcomer's own may pay, so a message never displaces another
3257    /// to the same recipient and a tie refuses the newcomer. Returns whether
3258    /// an entry was evicted.
3259    ///
3260    /// The failure this replaces: the outbox evicted the least recently sent
3261    /// entry to admit any new one, whatever either was. In run 5 of the
3262    /// 2026-10-19 volume test a ten-minute outage failed about 2,500 queued
3263    /// messages per phone this way, each one so that a newer message to the
3264    /// same unreachable peer could take its slot.
3265    pub(super) fn make_outbox_room(&mut self, newcomer: Option<&Message>) -> bool {
3266        let mut backlog: HashMap<&str, usize> = HashMap::new();
3267        for entry in self.outbox.values() {
3268            *backlog.entry(entry.message.recipient.as_str()).or_default() += 1;
3269        }
3270        let floor = newcomer.map(|message| {
3271            (
3272                message.recipient.as_str(),
3273                backlog
3274                    .get(message.recipient.as_str())
3275                    .copied()
3276                    .unwrap_or(0),
3277            )
3278        });
3279        let held = self.held_line_positions();
3280        let victim = self
3281            .outbox
3282            .iter()
3283            .filter(|(id, entry)| {
3284                let recipient = entry.message.recipient.as_str();
3285                let heavier = match floor {
3286                    Some((own, own_count)) => {
3287                        recipient != own && backlog.get(recipient).copied().unwrap_or(0) > own_count
3288                    }
3289                    None => true,
3290                };
3291                heavier && self.outbox_entry_evictable(id, entry)
3292            })
3293            .min_by(|(_, left), (_, right)| {
3294                let left_backlog = backlog
3295                    .get(left.message.recipient.as_str())
3296                    .copied()
3297                    .unwrap_or(0);
3298                let right_backlog = backlog
3299                    .get(right.message.recipient.as_str())
3300                    .copied()
3301                    .unwrap_or(0);
3302                right_backlog
3303                    .cmp(&left_backlog)
3304                    .then_with(|| {
3305                        Self::eviction_order_key(&held, left)
3306                            .cmp(&Self::eviction_order_key(&held, right))
3307                    })
3308                    .then_with(|| left.message.id.as_str().cmp(&right.message.id.as_str()))
3309            })
3310            .map(|(id, _)| id.clone());
3311        let Some(victim) = victim else {
3312            if newcomer.is_none() {
3313                // Every entry is being delivered, so the outbox runs over
3314                // its cap by internal frames until acknowledgements free
3315                // room. Bounded by the pending-acknowledgement table.
3316                warn!(
3317                    entries = self.outbox.len(),
3318                    "Outbox full of messages in delivery; admitting an internal frame over the cap"
3319                );
3320            }
3321            return false;
3322        };
3323        // Retired, not just removed: the retry and flush paths re-create a
3324        // missing entry before resending, so a retry entry left behind would
3325        // bring the message back.
3326        let (attempt_count, app_send) = self
3327            .retire_undeliverable_message(&victim, "outbox eviction")
3328            .map_or((0, false), |entry| (entry.attempt_count, entry.app_send));
3329        self.emit_outbox_capacity_failure(&victim, attempt_count, app_send);
3330        true
3331    }
3332
3333    /// The terminal events a message dropped for outbox capacity owes the
3334    /// app: `message_failed`, and for a connection request its typed
3335    /// undeliverable event. Shared by eviction and by a refused send.
3336    ///
3337    /// `app_send` decides whether the failure reaches telemetry; see
3338    /// [`Self::emit_settlement`].
3339    fn emit_outbox_capacity_failure(
3340        &mut self,
3341        message_id: &MessageId,
3342        attempt_count: u32,
3343        app_send: bool,
3344    ) {
3345        self.emit_settlement(
3346            Event::message_failed(
3347                message_id.clone(),
3348                "Outbox capacity exceeded",
3349                attempt_count,
3350            ),
3351            app_send,
3352        );
3353        if let Some(recipient) = self.take_undeliverable_connection_request(message_id) {
3354            warn!(
3355                recipient = %recipient,
3356                message_id = %message_id,
3357                "Connection request undeliverable: outbox capacity exceeded"
3358            );
3359            self.emit_event(Event::connection_request_undeliverable(
3360                recipient,
3361                message_id.as_str(),
3362                "outbox_capacity_exceeded",
3363            ));
3364        }
3365    }
3366
3367    /// Admission for a message the app sent, before anything is sealed again,
3368    /// held or transmitted. Returns false when the outbox is full and the
3369    /// message is refused; the caller then owes the app the terminal event
3370    /// (`message_failed`, "Outbox capacity exceeded", no attempts).
3371    ///
3372    /// A full outbox refuses the newest message for the recipient with the
3373    /// largest backlog rather than failing an older one, so a queued
3374    /// conversation stays contiguous and the app hears at the moment of
3375    /// sending. A message to a recipient with a smaller backlog is admitted by
3376    /// evicting from the largest one, so a week of messages to one
3377    /// unreachable peer cannot block a message to a reachable one.
3378    pub(super) fn admit_app_message(&mut self, message: &Message) -> bool {
3379        if !message.requires_ack
3380            || Self::is_media_outbox_message(message)
3381            || self.outbox.contains_key(&message.id)
3382            || self.outbox.len() < MAX_OUTBOX_ENTRIES
3383        {
3384            return true;
3385        }
3386        self.make_outbox_room(Some(message))
3387    }
3388
3389    /// [`Self::admit_app_message`] with the refusal settled: staged re-seal
3390    /// provenance dropped (it holds plaintext) and the terminal event emitted.
3391    /// Returns true when the caller should stop, having returned the id.
3392    fn refuse_app_message_at_capacity(&mut self, message: &Message) -> bool {
3393        if self.admit_app_message(message) {
3394            return false;
3395        }
3396        self.pending_reseal.remove(&message.id);
3397        warn!(
3398            message_id = %message.id,
3399            recipient = %message.recipient,
3400            "Outbox full and this recipient holds the largest backlog; send refused"
3401        );
3402        // Only the app send path refuses at capacity, so this is an app send.
3403        self.emit_outbox_capacity_failure(&message.id, 0, true);
3404        true
3405    }
3406
3407    pub(super) fn ensure_outbox_entry(&mut self, message: &Message) {
3408        if !message.requires_ack {
3409            return;
3410        }
3411
3412        let is_media = Self::is_media_outbox_message(message);
3413        let (outbox, capacity) = if is_media {
3414            use crate::constants::MAX_MEDIA_OUTBOX_ENTRIES;
3415            (&mut self.media_outbox, MAX_MEDIA_OUTBOX_ENTRIES)
3416        } else {
3417            (&mut self.outbox, MAX_OUTBOX_ENTRIES)
3418        };
3419
3420        if !outbox.contains_key(&message.id) && outbox.len() >= capacity {
3421            if is_media {
3422                // Media keeps its own cap and its oldest-first eviction: a
3423                // transfer has its own window, and an evicted chunk surfaces
3424                // through the transfer abort rather than a message event.
3425                let oldest_id = outbox
3426                    .iter()
3427                    .min_by_key(|(_, entry)| entry.last_sent_at)
3428                    .map(|(id, _)| id.clone());
3429                if let Some(oldest_id) = oldest_id {
3430                    // Retired, not just removed: this function is also how the
3431                    // retry and flush paths re-create an entry before resending,
3432                    // so a retry entry left behind here would bring the message
3433                    // back and evict another in its place.
3434                    self.retire_undeliverable_message(&oldest_id, "outbox eviction");
3435                }
3436            } else {
3437                // Reached only by frames that never passed app admission
3438                // (internal frames, and the defensive re-creation on the
3439                // resend paths): an app send made its room up front in
3440                // `admit_app_message`, or was refused there.
3441                self.make_outbox_room(None);
3442            }
3443        }
3444
3445        // Consume staged re-seal provenance only when first creating a
3446        // (non-media) entry here (the failure/retry path); later calls find the
3447        // entry already present and leave its `reseal` intact.
3448        let staged_reseal = self.take_staged_reseal(message, is_media);
3449        let app_send = !is_media && self.is_staged_app_send(message);
3450        let outbox = if is_media {
3451            &mut self.media_outbox
3452        } else {
3453            &mut self.outbox
3454        };
3455        let is_new = !outbox.contains_key(&message.id);
3456        outbox
3457            .entry(message.id.clone())
3458            .or_insert_with(|| OutboxEntry {
3459                message: message.clone(),
3460                attempt_count: 0,
3461                first_sent_at: Utc::now(),
3462                last_sent_at: Utc::now(),
3463                last_transport: None,
3464                reseal: staged_reseal,
3465                relay_pushed: false,
3466                app_send,
3467            });
3468        // Persist newly-created main-outbox entries so they survive a restart.
3469        // media entries are intentionally not persisted.
3470        if is_new && !is_media {
3471            if let Some(entry) = self.outbox.get(&message.id) {
3472                self.persist_outbox_entry(entry);
3473            }
3474        }
3475    }
3476
3477    pub(super) fn mark_message_sent(
3478        &mut self,
3479        message: &Message,
3480        transport: Option<TransportType>,
3481        attempt_hint: Option<u32>,
3482    ) {
3483        if !message.requires_ack {
3484            return;
3485        }
3486
3487        let now = Utc::now();
3488        let is_media = Self::is_media_outbox_message(message);
3489        // The success path creates the entry here, not in
3490        // `ensure_outbox_entry`, so it honours the cap here too. An app send
3491        // already made its room in `admit_app_message`; this is for internal
3492        // frames, which are never refused.
3493        if !is_media
3494            && !self.outbox.contains_key(&message.id)
3495            && self.outbox.len() >= MAX_OUTBOX_ENTRIES
3496        {
3497            self.make_outbox_room(None);
3498        }
3499        // Consume the staged re-seal provenance only when first creating a
3500        // (non-media) outbox entry; on later attempts the entry already carries
3501        // it. Staging for media is never populated.
3502        let staged_reseal = self.take_staged_reseal(message, is_media);
3503        let app_send = !is_media && self.is_staged_app_send(message);
3504        let outbox = if is_media {
3505            &mut self.media_outbox
3506        } else {
3507            &mut self.outbox
3508        };
3509        let is_new = !outbox.contains_key(&message.id);
3510        let entry = outbox
3511            .entry(message.id.clone())
3512            .or_insert_with(|| OutboxEntry {
3513                message: message.clone(),
3514                attempt_count: 0,
3515                first_sent_at: now,
3516                last_sent_at: now,
3517                last_transport: transport,
3518                reseal: staged_reseal,
3519                relay_pushed: false,
3520                app_send,
3521            });
3522
3523        entry.message = message.clone();
3524        if entry.attempt_count == 0 {
3525            entry.first_sent_at = now;
3526        }
3527        entry.attempt_count = attempt_hint.unwrap_or(entry.attempt_count.saturating_add(1));
3528        entry.last_sent_at = now;
3529        entry.last_transport = transport;
3530
3531        // Persist only when the entry is newly created here. The success path
3532        // never calls ensure_outbox_entry, so this is the one point a
3533        // successfully-sent message first enters the durable outbox before its
3534        // ACK. Later attempts don't re-persist: the durable copy already exists
3535        // and attempt/timestamp churn isn't restore-critical (the TTL is
3536        // refreshed on restore anyway), which keeps secure-storage writes off
3537        // the retry hot path. Media entries are never persisted.
3538        if is_new && !is_media {
3539            if let Some(entry) = self.outbox.get(&message.id) {
3540                self.persist_outbox_entry(entry);
3541            }
3542        }
3543    }
3544
3545    /// Stages re-seal provenance for a message about to be dispatched, so the
3546    /// outbox entry created during dispatch carries it. Callers gate on the
3547    /// content being a sealed encrypted DM (`__MLS_ENC__`); media and plaintext
3548    /// never stage and thus always replay verbatim.
3549    /// Stages `reseal` for message `id`, just sealed for `recipient`. Called
3550    /// straight after the seal, before anything else is sealed, so the
3551    /// session's latest seal is this message's.
3552    pub(super) fn stage_outbox_reseal(
3553        &mut self,
3554        id: &MessageId,
3555        recipient: &str,
3556        mut reseal: OutboxReseal,
3557    ) {
3558        reseal.seal_mark = self.last_seal_mark(recipient);
3559        self.pending_reseal.insert(id.clone(), reseal);
3560    }
3561
3562    fn last_seal_mark(&self, recipient: &str) -> Option<offline_protocol_mls::SealMark> {
3563        self.mls_manager
3564            .as_ref()?
3565            .read()
3566            .ok()?
3567            .last_seal_mark(recipient)
3568    }
3569
3570    /// Consumes any staged re-seal provenance for `message`, attaching it to a
3571    /// (non-media) main-outbox entry being created for the first time. On later
3572    /// attempts the entry already exists and carries its `reseal`, so this
3573    /// returns `None`. Media never stages.
3574    ///
3575    /// **Always removes the staged copy** (it does not leave it behind when it
3576    /// returns `None`): the staging map is strictly transient and must never
3577    /// outlive its message, or a message that stages but is then dropped before
3578    /// an outbox entry is created (e.g. `dispatch_prepared_message` early-returns
3579    /// on a duplicate id) would strand plaintext in `pending_reseal` for the
3580    /// process lifetime. Removing unconditionally here — plus the belt-and-braces
3581    /// clear in [`Self::remove_outbox_entry`] — bounds the map to genuinely
3582    /// in-flight sends. Kept as one helper so the two outbox-entry creation sites
3583    /// ([`Self::ensure_outbox_entry`] and [`Self::mark_message_sent`]) stay in
3584    /// lockstep on the consume condition.
3585    fn take_staged_reseal(&mut self, message: &Message, is_media: bool) -> Option<OutboxReseal> {
3586        let staged = self.pending_reseal.remove(&message.id);
3587        if is_media || self.outbox.contains_key(&message.id) {
3588            return None;
3589        }
3590        staged
3591    }
3592
3593    /// Re-seals an outbound encrypted DM against the recipient's *current* MLS
3594    /// session for a resend, returning fresh sealed content, or `None` to replay
3595    /// verbatim. Returns `None` when it is not a re-sealable encrypted DM, when
3596    /// there is no stored provenance (plaintext, media, or a pre-upgrade
3597    /// persisted entry), when the session is not currently confirmed, or when
3598    /// the re-seal produced identical bytes.
3599    ///
3600    /// This is the Tier 2 core: after a desync + re-key the recipient's session
3601    /// is rebuilt at a new epoch, so replaying the ciphertext sealed at the old
3602    /// epoch is undecryptable forever. Re-sealing here rebuilds only the
3603    /// ciphertext `content`; the outer `Message.id` is preserved so the
3604    /// receiver's dedup and ACK correlation are unaffected. Gating on a
3605    /// confirmed session guarantees `prepare_outbound_content` takes the
3606    /// encrypt-now path and never the `store_pending` queueing path.
3607    fn reseal_resend_content(&mut self, message: &Message) -> Option<String> {
3608        if !message.content.starts_with(internal_prefixes::ENCRYPTED) {
3609            return None;
3610        }
3611        let recipient = message.recipient.as_str().to_string();
3612        if !self.confirmed_sessions.contains(&recipient) {
3613            return None;
3614        }
3615        let reseal = self.outbox.get(&message.id)?.reseal.as_ref()?.clone();
3616        // Every re-seal spends a ratchet generation the receiver has to
3617        // fast-forward over, and one it drops as a duplicate is never
3618        // decrypted at all. Past the budget, a resend goes out verbatim: it
3619        // costs nothing, and it still decrypts unless the receiver has since
3620        // moved more than the out-of-order tolerance past it. Without this, a
3621        // link that stops carrying acknowledgements turns the retry ladder
3622        // into a key-burner: in the 2026-10-19 rerun one phone re-sealed
3623        // 20,919 resends during a 17-minute stall and left its peer past even
3624        // the widened forward window.
3625        let unacknowledged = self
3626            .mls_manager
3627            .as_ref()
3628            .and_then(|mls| mls.read().ok())
3629            .map_or(0, |mls| mls.unacknowledged_seals(&recipient));
3630        // A verbatim copy this far behind cannot decrypt any more, so past
3631        // the budget it is still re-sealed, up to the hard limit (see
3632        // `RESEAL_HARD_LIMIT`).
3633        let verbatim_is_dead = match (reseal.seal_mark, self.last_seal_mark(&recipient)) {
3634            (Some(sealed_at), Some(newest)) => {
3635                sealed_at.incarnation == newest.incarnation
3636                    && newest.index.saturating_sub(sealed_at.index)
3637                        > u64::from(
3638                            offline_protocol_mls::group::SENDER_RATCHET_OUT_OF_ORDER_TOLERANCE,
3639                        )
3640            }
3641            _ => false,
3642        };
3643        let budget_spent = if verbatim_is_dead {
3644            unacknowledged >= RESEAL_HARD_LIMIT
3645        } else {
3646            unacknowledged >= RESEAL_GAP_BUDGET
3647        };
3648        if budget_spent {
3649            debug!(
3650                message_id = %message.id,
3651                recipient = %recipient,
3652                unacknowledged,
3653                "Re-seal budget spent; resending verbatim until the peer acknowledges"
3654            );
3655            return None;
3656        }
3657        match self.prepare_outbound_content(
3658            &recipient,
3659            &reseal.content,
3660            reseal.priority,
3661            reseal.reply_to_msg.clone(),
3662            reseal.forwarded_from.clone(),
3663            reseal.content_type,
3664            reseal.media_metadata.clone(),
3665            reseal.rich.as_ref(),
3666            Some(&message.app_id),
3667            Some(PendingProvenance::for_id(message.id.clone())),
3668            "resend_reseal",
3669        ) {
3670            Ok(OutboundSendPreparation::Ready(sealed)) if sealed != message.content => {
3671                let mark = self.last_seal_mark(&recipient);
3672                if let Some(provenance) = self
3673                    .outbox
3674                    .get_mut(&message.id)
3675                    .and_then(|entry| entry.reseal.as_mut())
3676                {
3677                    provenance.seal_mark = mark;
3678                }
3679                Some(sealed)
3680            }
3681            // Identical bytes, or an error: replay verbatim this round. Tier 1
3682            // keeps the sender retrying until the session heals.
3683            //
3684            // The `Queued` arm is all but unreachable — the `confirmed_sessions`
3685            // gate above runs synchronously with no yield before the encrypt, so
3686            // the session cannot flip to not-ready in between. If it ever does
3687            // (e.g. `confirmed_sessions` outlived an externally-deleted MLS
3688            // session), `prepare_outbound_content` has enqueued a pending message
3689            // as a side effect, keyed by our SAME `existing_id`. That is benign:
3690            // this round still replays the verbatim outbox bytes, and when the
3691            // pending copy later flushes, the sender's own `dispatch_prepared_message`
3692            // dedup drops it as a duplicate id. So no double-delivery, just a
3693            // transient pending entry that self-clears on flush.
3694            _ => None,
3695        }
3696    }
3697
3698    /// Re-seals `message` in place against the current session if it is a
3699    /// re-sealable encrypted DM (see [`Self::reseal_resend_content`]). Used at
3700    /// the resend transmit points so the bytes that go out — and the outbox copy
3701    /// `mark_message_sent` then stores — are freshly sealed.
3702    pub(super) fn reseal_for_resend_in_place(&mut self, message: &mut Message) {
3703        if let Some(fresh) = self.reseal_resend_content(message) {
3704            message.content = fresh;
3705        }
3706    }
3707
3708    pub(super) fn remove_outbox_entry(&mut self, message_id: &MessageId) -> Option<OutboxEntry> {
3709        // Belt-and-braces: drop any still-staged re-seal provenance for this id
3710        // so it never outlives the message (see `take_staged_reseal`). Normally
3711        // staging is consumed at entry creation; this covers the id being torn
3712        // down before that ever happened.
3713        self.pending_reseal.remove(message_id);
3714        self.undecryptable_resent.remove(message_id);
3715        // A receipt names an outbox entry; with the entry gone it names
3716        // nothing, and keeping it would only grow the map.
3717        self.custody_receipts.remove(message_id);
3718        if let Some(entry) = self.outbox.remove(message_id) {
3719            if !Self::is_confirmation_probe(&entry.message) {
3720                self.clear_outbox_entry_from_storage(message_id);
3721            }
3722            return Some(entry);
3723        }
3724        self.media_outbox.remove(message_id)
3725    }
3726
3727    /// Takes `message_id` out of the three places that can send it again:
3728    /// the retry queue, the pending-ACK tracker and the outbox. One without
3729    /// the others resurrects it (see [`Self::retire_undeliverable_message`]).
3730    /// No failure is recorded against the carrier: that is the give-up
3731    /// path's, and a message withdrawn for being superseded did not fail.
3732    pub(super) fn forget_outbound_message(
3733        &mut self,
3734        message_id: &MessageId,
3735    ) -> Option<OutboxEntry> {
3736        self.retry_queue.remove(&message_id.as_str());
3737        self.ack_manager.remove_ack(message_id);
3738        self.remove_outbox_entry(message_id)
3739    }
3740
3741    /// Takes a message this device has given up on out of everything that
3742    /// could send it again, returning its outbox entry if it still had one.
3743    ///
3744    /// A settlement is only terminal if nothing can bring the message back
3745    /// afterwards, and three things can: the retry queue, the pending-ACK
3746    /// tracker (whose timeout re-queues it), and the outbox. The retry and
3747    /// flush paths re-create a missing outbox entry before resending, with
3748    /// fresh clocks, so an id dropped from the outbox but left queued for
3749    /// retry is never expired by anything. At capacity each of those resends
3750    /// evicted a live message to make room, and that message's own retry entry
3751    /// did the same: a device holding more than a full outbox of undeliverable
3752    /// messages reported a "terminal" `message_failed` on every retry,
3753    /// indefinitely, for ids it had already settled. An id left in the ACK
3754    /// tracker is settled a second time when its timeout finds no entry.
3755    ///
3756    /// Every give-up path calls this, so the teardown is written once. It
3757    /// emits nothing: which event a drop owes the application differs by
3758    /// path, and stays with the caller. `media_reason` is what the aborted
3759    /// transfer reports when the id is a media chunk.
3760    pub(super) fn retire_undeliverable_message(
3761        &mut self,
3762        message_id: &MessageId,
3763        media_reason: &str,
3764    ) -> Option<OutboxEntry> {
3765        self.handle_outbound_media_chunk_failed(message_id, media_reason);
3766        let entry = self.forget_outbound_message(message_id);
3767        if let Some(transport) = entry.as_ref().and_then(|entry| entry.last_transport) {
3768            self.transport_manager.record_delivery_failure(transport);
3769        }
3770        entry
3771    }
3772
3773    /// Settles the pending connection-request entry for `message_id` when a
3774    /// terminal drop occurs (max retries, outbox expiry, capacity eviction),
3775    /// returning the recipient the caller should emit
3776    /// `connection_request_undeliverable` for. The TTL gates emission — a
3777    /// signal that stale belongs to a request the app has long stopped
3778    /// waiting on — but the entry is removed either way.
3779    pub(super) fn take_undeliverable_connection_request(
3780        &mut self,
3781        message_id: &MessageId,
3782    ) -> Option<String> {
3783        self.pending_connection_requests
3784            .remove(&message_id.as_str())
3785            .filter(|pending| pending.sent_at.elapsed() <= PENDING_CONNECTION_REQUEST_TTL)
3786            .map(|pending| pending.recipient)
3787    }
3788
3789    /// Removes messages that have waited too long for MLS session
3790    /// establishment. This gives the pre-outbox queue the same bounded
3791    /// lifecycle guarantee as dispatched messages.
3792    pub(super) fn cleanup_expired_pending_messages(&mut self) {
3793        self.cleanup_expired_pending_messages_at(Utc::now());
3794    }
3795
3796    /// Runs pending-message expiry only when the earliest queued deadline is
3797    /// due. Bindings call `process()` every 100 ms, so blindly scanning the
3798    /// entire queue there wastes CPU and battery for almost the whole default
3799    /// seven-day lifetime.
3800    pub(super) fn cleanup_expired_pending_messages_if_due(&mut self) {
3801        let now = Utc::now();
3802        if self
3803            .next_pending_message_expiry
3804            .is_some_and(|expiry| now >= expiry)
3805        {
3806            self.cleanup_expired_pending_messages_at(now);
3807        }
3808    }
3809
3810    fn cleanup_expired_pending_messages_at(&mut self, now: DateTime<Utc>) {
3811        let lifetime_ms = self
3812            .config
3813            .reliability
3814            .retry
3815            .pending_message_max_lifetime_ms;
3816        let mut changed_recipients = Vec::new();
3817        let mut expired_ids = Vec::new();
3818
3819        for (recipient, messages) in &mut self.pending_encrypted_messages {
3820            let previous_len = messages.len();
3821            messages.retain(|pending| {
3822                // Bounded per pass: each expiry costs a durable delete, and a
3823                // burst of entries queued together comes due together. The
3824                // remainder stays queued past its deadline and drains on the
3825                // next tick — see `MAX_PENDING_EXPIRIES_PER_PASS`.
3826                if expired_ids.len() < MAX_PENDING_EXPIRIES_PER_PASS
3827                    && lifetime_expired(now, pending.queued_at, lifetime_ms)
3828                {
3829                    expired_ids.push(pending.message_id.clone());
3830                    false
3831                } else {
3832                    true
3833                }
3834            });
3835            if messages.len() != previous_len {
3836                changed_recipients.push(recipient.clone());
3837            }
3838        }
3839
3840        for recipient in changed_recipients {
3841            if self
3842                .pending_encrypted_messages
3843                .get(&recipient)
3844                .is_some_and(Vec::is_empty)
3845            {
3846                self.pending_encrypted_messages.remove(&recipient);
3847            }
3848        }
3849        // The survivors' records are untouched, so only the expired entries are
3850        // written to storage — one delete each, paired with the settlement
3851        // below.
3852        self.delete_pending_messages_from_storage(expired_ids.iter());
3853        self.recompute_next_pending_message_expiry();
3854
3855        for message_id in expired_ids {
3856            // Reached both from `process()` (emits immediately) and from
3857            // restore, which runs before the event pipeline is live.
3858            self.settle_restored_message_failure(Event::message_failed(
3859                message_id,
3860                "Pending session lifetime exceeded",
3861                0,
3862            ));
3863        }
3864    }
3865
3866    pub(super) fn cleanup_outbox(&mut self) {
3867        let now = Utc::now();
3868        let lifetime_ms = self.config.reliability.retry.outbox_max_lifetime_ms;
3869        // In-process twin of the restore path's absolute cap
3870        // (`restore_outbox`): the carrier-relative window slides on every
3871        // send, and an unreachable-parked DM's mesh reachability probe keeps
3872        // sending — without an absolute bound from `first_sent_at`, a
3873        // long-lived process would keep such an entry alive forever.
3874        let absolute_lifetime_ms =
3875            lifetime_ms.saturating_mul(crate::constants::OUTBOX_ABSOLUTE_LIFETIME_FACTOR as u64);
3876
3877        let mut expired_from_outbox = Vec::new();
3878        for (message_id, entry) in &self.outbox {
3879            // A relay-pushed entry's fixed window is a hard stop: it does not
3880            // wait out an in-flight probe, whose re-sends are exactly what
3881            // the window exists to end.
3882            let push_window_closed = super::relay_push_window_closed(now, entry);
3883            if !push_window_closed
3884                && !lifetime_expired(now, entry.last_sent_at, lifetime_ms)
3885                && !lifetime_expired(now, entry.first_sent_at, absolute_lifetime_ms)
3886            {
3887                continue;
3888            }
3889            if !push_window_closed
3890                && entry.message.requires_ack
3891                && self.ack_manager.is_waiting_for_ack(&entry.message.id)
3892            {
3893                continue;
3894            }
3895            expired_from_outbox.push((
3896                message_id.clone(),
3897                entry.attempt_count,
3898                entry.relay_pushed,
3899                entry.app_send,
3900            ));
3901        }
3902        for (message_id, attempt_count, relay_pushed, app_send) in expired_from_outbox {
3903            self.retire_undeliverable_message(&message_id, "outbox lifetime exceeded");
3904
3905            // Outcome unknown, so nothing app-facing: the push may already
3906            // have delivered it (the `park_relay_pushed_dm` contract), and
3907            // `MessageSent` already told the app it left. `message_failed`
3908            // would invite a manual resend under a fresh id that the
3909            // recipient's dedup cannot collapse. Only plain DMs are ever
3910            // flagged, so there is no connection request to settle either.
3911            if relay_pushed {
3912                debug!(
3913                    message_id = %message_id,
3914                    "Relay-pushed DM reached its redelivery window; dropping it silently"
3915                );
3916                continue;
3917            }
3918
3919            // Expiry is terminal: without an event the app shows the message
3920            // as pending forever. Mirrors handle_max_retries_exceeded, which
3921            // settles the same way when the retry budget runs out.
3922            self.emit_settlement(
3923                Event::message_failed(
3924                    message_id.clone(),
3925                    "Outbox lifetime exceeded",
3926                    attempt_count,
3927                ),
3928                app_send,
3929            );
3930            if let Some(recipient) = self.take_undeliverable_connection_request(&message_id) {
3931                warn!(
3932                    recipient = %recipient,
3933                    message_id = %message_id,
3934                    "Connection request undeliverable: outbox lifetime exceeded"
3935                );
3936                self.emit_event(Event::connection_request_undeliverable(
3937                    recipient,
3938                    message_id.as_str(),
3939                    "outbox_lifetime_exceeded",
3940                ));
3941            }
3942        }
3943
3944        // Only peers that still hold outbox entries can retain an
3945        // unreachable-park counter (bounds the map at the outbox cap).
3946        if !self.dm_unreachable_parks.is_empty() {
3947            let active: HashSet<&str> = self
3948                .outbox
3949                .values()
3950                .map(|entry| entry.message.recipient.as_str())
3951                .collect();
3952            self.dm_unreachable_parks
3953                .retain(|peer, _| active.contains(peer.as_str()));
3954        }
3955
3956        let mut expired_from_media = Vec::new();
3957        for (message_id, entry) in &self.media_outbox {
3958            if !lifetime_expired(now, entry.last_sent_at, lifetime_ms) {
3959                continue;
3960            }
3961            if entry.message.requires_ack && self.ack_manager.is_waiting_for_ack(&entry.message.id)
3962            {
3963                continue;
3964            }
3965            expired_from_media.push(message_id.clone());
3966        }
3967        for message_id in expired_from_media {
3968            self.retire_undeliverable_message(&message_id, "outbox lifetime exceeded");
3969        }
3970    }
3971
3972    pub(super) fn ensure_ack_registration(&mut self, message: &Message) -> Result<bool> {
3973        if !message.requires_ack {
3974            return Ok(false);
3975        }
3976
3977        if self.ack_manager.is_waiting_for_ack(&message.id) {
3978            Ok(false)
3979        } else {
3980            if let Some(eviction) = self
3981                .ack_manager
3982                .register_pending_ack(message.id.clone(), None)?
3983            {
3984                self.emit_event(Event::ack_evicted(
3985                    eviction.message_id,
3986                    eviction.priority.as_str(),
3987                    "capacity".to_string(),
3988                ));
3989            }
3990            Ok(true)
3991        }
3992    }
3993
3994    pub(super) fn is_media_outbox_message(message: &Message) -> bool {
3995        message.content_type == ContentType::FileChunk
3996    }
3997
3998    /// A frame that sets up or confirms a 1:1 session: a key package, a
3999    /// Welcome, or a confirmation probe or its answer. Never windowed (see
4000    /// [`Self::peer_window_governs`]).
4001    pub(super) fn is_session_control_frame(message: &Message) -> bool {
4002        let content = message.content.as_str();
4003        [
4004            internal_prefixes::KEY_PACKAGE,
4005            internal_prefixes::WELCOME,
4006            internal_prefixes::SESSION_CONFIRM_PROBE,
4007            internal_prefixes::SESSION_CONFIRM_ACK,
4008        ]
4009        .iter()
4010        .any(|prefix| content.starts_with(prefix))
4011    }
4012
4013    /// A session confirmation probe lives in memory only. It carries no user
4014    /// data, and a restarted process does not know which probe was the last,
4015    /// so a restored one could never be superseded; the next scan asks the
4016    /// same question. Persisting it cost a secure-storage write per probe and
4017    /// a delete per supersede, under the protocol lock.
4018    pub(super) fn is_confirmation_probe(message: &Message) -> bool {
4019        message
4020            .content
4021            .starts_with(internal_prefixes::SESSION_CONFIRM_PROBE)
4022    }
4023
4024    // ========================================================================
4025    // DELIVERY TRACKING
4026    // ========================================================================
4027
4028    /// Handles successful message send.
4029    /// Whether a frame for `peer_id` can arrive without a neighbour carrying
4030    /// it, given what this device knows about *this recipient*.
4031    ///
4032    /// [`TransportManager::can_reach_without_carrying`] answers the same
4033    /// question from carrier status alone, and so says yes for every recipient
4034    /// while any infrastructure carrier is up. This is that answer with the
4035    /// per-recipient facts applied: a carrier that has already told us it
4036    /// cannot reach this recipient stops counting as a way to reach them.
4037    ///
4038    /// Two properties hold by construction:
4039    ///
4040    /// - **A carrier holding no fact is unchanged.** Nostr cannot report
4041    ///   per-recipient delivery at all, so it never produces a fact and keeps
4042    ///   its blanket claim. That is deliberate: the mixed-neighbourhood
4043    ///   residual stays open for Nostr-only devices rather than closing by
4044    ///   accident under a design that never gave them a verdict to act on.
4045    /// - **A live mesh link always wins.** It is ground truth rather than a
4046    ///   claim, and it is checked whether or not any fact exists.
4047    pub(crate) fn can_reach_recipient(&self, peer_id: &str) -> bool {
4048        let now = Instant::now();
4049        let infrastructure_reaches_peer = self
4050            .transport_manager
4051            .available_infrastructure_carriers()
4052            .into_iter()
4053            .any(|carrier| {
4054                !matches!(
4055                    self.reachability.claim_for(peer_id, carrier, now),
4056                    Some(Claim::Unreachable)
4057                )
4058            });
4059        infrastructure_reaches_peer || self.transport_manager.mesh_can_address(peer_id)
4060    }
4061
4062    /// Holds `message` back when the pending-ACK table has no slot for it,
4063    /// returning when the retry queue will try it again; `None` means it has a
4064    /// slot and the caller transmits as usual.
4065    ///
4066    /// The check runs **before** the transmit, because nothing honest is left
4067    /// to do after it. A frame sent without a slot is either untracked (its
4068    /// loss is never noticed) or reported to the app as a failed send while it
4069    /// is on the wire, which is what the 2026-10-19 volume test saw: the
4070    /// receiver got 4,626 messages from a sender whose API had accepted 2,748,
4071    /// and the retry path's copy of the same error aborted every later
4072    /// `process()` tick, so the step that frees slots never ran again.
4073    ///
4074    /// A held message goes to the outbox and the retry queue at its first
4075    /// attempt, so holding it spends nothing from its retry budget, and it is
4076    /// not offered to the mesh: it was not sent, so nothing failed.
4077    pub(super) fn defer_for_ack_capacity(&mut self, message: &Message) -> Option<DateTime<Utc>> {
4078        if !message.requires_ack || self.ack_manager.has_capacity_for(&message.id) {
4079            return None;
4080        }
4081        self.ensure_outbox_entry(message);
4082        let next_retry_at = self
4083            .retry_queue
4084            .enqueue(message.clone(), 0)
4085            .unwrap_or_else(Utc::now);
4086        debug!(
4087            message_id = %message.id,
4088            pending_acks = self.ack_manager.pending_count(),
4089            "Pending-ACK table full; message held for the retry queue"
4090        );
4091        Some(next_retry_at)
4092    }
4093
4094    /// [`Self::defer_for_ack_capacity`] for a message the app sent, which is
4095    /// owed a `message_deferred` so it does not read as stuck.
4096    fn defer_app_message_for_ack_capacity(&mut self, message: &Message) -> bool {
4097        let Some(next_retry_at) = self.defer_for_ack_capacity(message) else {
4098            return false;
4099        };
4100        self.emit_event(Event::message_deferred(
4101            message.id.clone(),
4102            message.recipient.as_str().to_string(),
4103            SEND_DEFER_REASON_ACK_CAPACITY,
4104            0,
4105            Some(next_retry_at.timestamp_millis()),
4106        ));
4107        true
4108    }
4109
4110    // ------------------------------------------------------------------------
4111    // Per-peer send window (see `PEER_SEND_WINDOW`)
4112    // ------------------------------------------------------------------------
4113
4114    /// Frames to `recipient` that went out over a mesh carrier and still
4115    /// await its acknowledgement. Derived from the outbox rather than kept as
4116    /// a count, so no settle, expiry or eviction path can leak a slot.
4117    pub(super) fn mesh_frames_in_flight_to(&self, recipient: &str) -> usize {
4118        self.outbox
4119            .values()
4120            .filter(|entry| {
4121                entry.attempt_count > 0
4122                    && entry.message.recipient.as_str() == recipient
4123                    && matches!(
4124                        entry.last_transport,
4125                        Some(TransportType::BLE | TransportType::WiFiDirect)
4126                    )
4127            })
4128            .count()
4129    }
4130
4131    /// Whether `message` is one the window governs: a frame that waits for an
4132    /// acknowledgement, is not a media chunk (transfers have their own
4133    /// window), is not session control, and goes to a peer this device holds
4134    /// a live mesh link to.
4135    ///
4136    /// Session control is exempt because it is what heals the session the
4137    /// window's frames are sealed under. A key package or Welcome held behind
4138    /// sixteen frames the peer cannot decrypt waits for acknowledgements that
4139    /// never come, and the window then stays full of frames that are only
4140    /// retried, never answered.
4141    fn peer_window_governs(&self, message: &Message) -> bool {
4142        message.requires_ack
4143            && !Self::is_media_outbox_message(message)
4144            && !Self::is_session_control_frame(message)
4145            && self
4146                .transport_manager
4147                .mesh_transport_for(message.recipient.as_str())
4148                .is_some()
4149    }
4150
4151    /// Whether `message` may be transmitted now. A frame already in flight
4152    /// always may (its resend is not a new slot); a new one only while the
4153    /// peer has fewer than [`PEER_SEND_WINDOW`] frames in flight.
4154    pub(super) fn peer_window_admits(&self, message: &Message) -> bool {
4155        if !self.peer_window_governs(message) {
4156            return true;
4157        }
4158        if self
4159            .outbox
4160            .get(&message.id)
4161            .is_some_and(|entry| entry.attempt_count > 0)
4162        {
4163            return true;
4164        }
4165        self.mesh_frames_in_flight_to(message.recipient.as_str()) < PEER_SEND_WINDOW
4166    }
4167
4168    /// Parks `message` in the outbox, out of the retry queue, at the back of
4169    /// its recipient's held line. No attempt is counted and nothing is
4170    /// sealed again: being held is not an attempt.
4171    pub(super) fn hold_for_peer_window(&mut self, message: &Message) {
4172        self.ensure_outbox_entry(message);
4173        self.retry_queue.remove(&message.id.as_str());
4174        let held = self
4175            .window_held
4176            .entry(message.recipient.as_str().to_string())
4177            .or_default();
4178        if !held.contains(&message.id) {
4179            held.push_back(message.id.clone());
4180        }
4181    }
4182
4183    /// The window gate for a message the app just sent. Holds it when the
4184    /// peer's window is full, or when earlier messages to that peer are
4185    /// already held, so a new message never overtakes them. Emits
4186    /// `message_deferred` so the app can show it as queued.
4187    fn defer_app_message_for_peer_window(&mut self, message: &Message) -> bool {
4188        if !self.peer_window_governs(message) {
4189            return false;
4190        }
4191        let held_ahead = self
4192            .window_held
4193            .get(message.recipient.as_str())
4194            .is_some_and(|held| !held.is_empty());
4195        if !held_ahead && self.peer_window_admits(message) {
4196            return false;
4197        }
4198        self.hold_for_peer_window(message);
4199        debug!(
4200            message_id = %message.id,
4201            recipient = %message.recipient,
4202            "Peer send window full; message held until an acknowledgement frees it"
4203        );
4204        self.emit_event(Event::message_deferred(
4205            message.id.clone(),
4206            message.recipient.as_str().to_string(),
4207            SEND_DEFER_REASON_PEER_WINDOW,
4208            0,
4209            None,
4210        ));
4211        true
4212    }
4213
4214    pub(super) fn handle_send_success(
4215        &mut self,
4216        message: &Message,
4217        transport: Option<TransportType>,
4218    ) -> Result<()> {
4219        self.mark_message_sent(message, transport, Some(1));
4220        // Never `?`: the frame is already on the wire, so an error here would
4221        // report a transmitted message as failed. Every transmit path asks
4222        // `defer_for_ack_capacity` first, so this is only reachable through a
4223        // priority eviction race, and a warning is the honest outcome.
4224        if let Err(err) = self.ensure_ack_registration(message) {
4225            warn!(message_id = %message.id, error = %err, "ACK registration failed after transmit");
4226        }
4227
4228        // A transport reporting success is not always evidence the frame can
4229        // arrive. The internet transport and Reticulum accept any recipient
4230        // and return `Ok`, so a send to someone neither can reach is reported
4231        // as sent and never arrives, and because the mesh hand-off used to hang
4232        // off the *failure* path, a device with either carrier up would swallow
4233        // the frame instead of asking its neighbors to carry it. That is precisely
4234        // the out-of-range case forwarding exists for, so the question is asked
4235        // directly rather than inferred from an error that never comes.
4236        //
4237        // Offering here is additive, never a replacement: the outbox entry and
4238        // the ACK registration above stand either way, and if the accepting
4239        // carrier did deliver after all, the recipient's deduplicator absorbs
4240        // the second copy.
4241        if !self.can_reach_recipient(message.recipient.as_str()) {
4242            let handed_to_mesh = self.offer_to_mesh(message);
4243            if handed_to_mesh > 0 {
4244                debug!(
4245                    message_id = %message.id,
4246                    recipient = %message.recipient,
4247                    handed_to_mesh,
4248                    "Transport accepted a recipient it cannot reach; also handed to neighbors"
4249                );
4250            }
4251        }
4252        Ok(())
4253    }
4254
4255    /// Handles failed message send by persisting to outbox and scheduling retry.
4256    ///
4257    /// EDGE CASE HANDLING:
4258    /// - Ensures message is persisted to outbox for recovery
4259    /// - Schedules retry with exponential backoff
4260    /// - Handles case where all transports are unavailable
4261    ///
4262    /// NOTE: This does NOT call `record_retry_failure` — callers that need it
4263    /// (e.g. `send_via_forced_transport`) must record the failure themselves.
4264    /// `TransportManager::send()` already records failures internally, so
4265    /// calling it here would double-count.
4266    /// Returns the absolute time the retry was scheduled for (`None` when the
4267    /// message was already queued for retry), so callers can surface it in
4268    /// `MessageDeferred` instead of guessing.
4269    pub(super) fn handle_send_failure(
4270        &mut self,
4271        message: &Message,
4272        transport: Option<TransportType>,
4273    ) -> Result<Option<DateTime<Utc>>> {
4274        // Ensure message is persisted to outbox for recovery
4275        self.ensure_outbox_entry(message);
4276
4277        // No transport could reach the recipient directly — but a neighbor
4278        // might be able to, or know someone who can. Offer the frame to the
4279        // mesh before settling for a retry: this is the case the whole
4280        // forwarding path exists for, someone out of our range but inside the
4281        // crowd's.
4282        //
4283        // The outbox entry and retry stay in place regardless. Handing a frame
4284        // to neighbors is not proof it arrived, and the acknowledgement coming
4285        // back is what settles it.
4286        let handed_to_mesh = self.offer_to_mesh(message);
4287
4288        // Schedule retry (enqueue is infallible — no attempt limit)
4289        let next_retry_at = self.retry_queue.enqueue(message.clone(), 0);
4290
4291        warn!(
4292            message_id = %message.id,
4293            transport = ?transport,
4294            handed_to_mesh,
4295            "Deferred message due to send error"
4296        );
4297        Ok(next_retry_at)
4298    }
4299
4300    /// The mesh offer for a **resend**: the retry queue and both outbox
4301    /// flushes. A resend asks the same question a first send does, so it gets
4302    /// the same answer as [`Self::handle_send_success`] and
4303    /// [`Self::handle_send_failure`]: a send no carrier took is offered to the
4304    /// neighbors, and so is one a carrier took for a recipient it cannot
4305    /// reach.
4306    ///
4307    /// Without this a message whose first attempt a carrier accepted and lost
4308    /// (a stream that was already dead and had not noticed) never reached the
4309    /// mesh at all: by the time its acknowledgement timed out the direct link
4310    /// was gone, every resend was refused, and each refusal only went back on
4311    /// the retry queue while a neighbor that could reach the recipient sat
4312    /// unused for the life of the outbox entry.
4313    ///
4314    /// Re-offering on every resend is bounded for the reasons the park path
4315    /// gives: the retry backoff bounds how often we ask, each offer spends the
4316    /// own-send tokens any other frame of ours does, and a neighbor that took
4317    /// the frame carries our re-send again only within its per-neighbor
4318    /// allowance for us (it has to: the recipient re-acknowledges only a
4319    /// re-send that reaches it). Every device further on still suppresses the
4320    /// id for the `RelaySeenCache` retention window. Excluding neighbors we already handed it to
4321    /// would be wrong, because a neighbor records an id only when it *accepts*
4322    /// the frame, and one that refused it (rate limit, full queue) is exactly
4323    /// the one a later offer should reach.
4324    pub(super) fn offer_resend_to_mesh(&mut self, message: &Message, accepted: bool) -> usize {
4325        if !self.needs_mesh_copy(message.recipient.as_str(), accepted) {
4326            return 0;
4327        }
4328        let handed_to_mesh = self.offer_to_mesh(message);
4329        if handed_to_mesh > 0 {
4330            debug!(
4331                message_id = %message.id,
4332                recipient = %message.recipient,
4333                accepted,
4334                handed_to_mesh,
4335                "Resend could not reach the recipient directly; handed it to neighbors"
4336            );
4337        }
4338        handed_to_mesh
4339    }
4340
4341    /// Whether a frame for `recipient` that a carrier did (`accepted`) or did
4342    /// not take must also be handed to the neighbours.
4343    ///
4344    /// The one rule for a resend and a handshake frame: yes when no carrier
4345    /// took it, and yes when one took it for a recipient that
4346    /// [`Self::can_reach_recipient`] says no carrier reaches, because a
4347    /// carrier's `Ok` is not delivery. Two copies of it drifting apart would
4348    /// leave one path swallowing frames the other carries.
4349    fn needs_mesh_copy(&self, recipient: &str, accepted: bool) -> bool {
4350        !accepted || !self.can_reach_recipient(recipient)
4351    }
4352
4353    /// Hands a locally-originated frame to nearby devices so it can travel
4354    /// toward a recipient we cannot reach ourselves.
4355    ///
4356    /// Returns how many neighbors took a copy. Zero means we are alone, or
4357    /// every neighbor refused the link — in both cases the retry ladder and the
4358    /// outbox remain the recovery path, exactly as before.
4359    ///
4360    /// Unlike a forwarded frame, this one is not held back: the delay before
4361    /// forwarding exists so neighbors holding the *same* frame do not all
4362    /// transmit at once, and nobody else is holding this one yet.
4363    pub(super) fn offer_to_mesh(&mut self, message: &Message) -> usize {
4364        // Deliberately not gated on `allow_relay`. That setting is about what
4365        // this device does for *other people* — whether it spends its battery
4366        // carrying their traffic. This is the device's own message, and the
4367        // ones it must be able to send include the acknowledgement for a
4368        // message it just received. Gating it here would leave a
4369        // relay-declining device unable to answer anything that reached it
4370        // across the mesh, so its sender would retransmit to exhaustion and
4371        // report a failure for a message that was delivered and read.
4372
4373        // Never hand a self-addressed frame to the mesh. Those are the relay
4374        // hint frames (`__GRP_RELAY_REG__`, `__GRP_RELAY_BCAST__`), which mean
4375        // something only to our own bridge: a neighbor receiving one would see
4376        // a frame addressed to somebody else and carry it further, spending
4377        // airtime to deliver a control op nobody can act on. They are pinned to
4378        // Internet for the same reason.
4379        if message.recipient.as_str() == self.local_id {
4380            return 0;
4381        }
4382
4383        let neighbors = self.transport_manager.mesh_neighbors();
4384        if neighbors.is_empty() {
4385            return 0;
4386        }
4387
4388        // Ours to originate: record it as handled so a copy coming back to us
4389        // through the mesh is recognized and not forwarded again.
4390        self.mesh_relay.mark_handled(&message.id.as_str());
4391
4392        // If the recipient is standing right there, hand it to them. Reached
4393        // here it means the ordinary send could not use that link, but trying
4394        // it costs one call and beats routing around the destination.
4395        let recipient = message.recipient.as_str();
4396        let targets = if neighbors.iter().any(|n| n.peer_id == recipient) {
4397            vec![recipient.to_string()]
4398        } else {
4399            // Full width regardless of this device's battery. Capability bias
4400            // narrows the fan-out for *forwarded* frames, where neighbors hold
4401            // copies and the sender is still retrying so the cost is
4402            // redundancy. This frame is ours and nobody else is holding it, so
4403            // the fan-out is the delivery attempt itself — and since targets
4404            // are chosen stably per message id, a narrowed set would be the
4405            // same narrowed set on every retransmission.
4406            self.mesh_relay.select_origination_targets(
4407                neighbors
4408                    .iter()
4409                    .map(|n| (n.peer_id.as_str(), n.link_quality())),
4410                &[],
4411                &message.id.as_str(),
4412            )
4413        };
4414
4415        // The deposit request (`docs/spec/custody.md`, "What the depositor
4416        // writes"): a class token on this device's own sealed replication
4417        // frame, judged from the plaintext retained for re-sealing, written
4418        // only here, where the frame is handed to proven mesh neighbours
4419        // because the recipient is out of reach. Never toward a custodian
4420        // whose receipt for this frame is still live.
4421        let custody_class = self.custody_class_for(message);
4422
4423        let mut handed_to = 0usize;
4424        for target in targets {
4425            // Metered against the same ceiling as carrying other people's
4426            // traffic: it is the same radio. Without this a large transfer,
4427            // which offers every chunk separately, would put an unbounded burst
4428            // on the air in one call.
4429            //
4430            // Own traffic spends from the reserve that forwarding leaves alone,
4431            // so a device carrying the neighborhood at its ceiling can still
4432            // get its own frames out — an acknowledgement above all, whose
4433            // absence makes a delivered message look lost to its sender.
4434            if !self.mesh_relay.take_own_send_token() {
4435                debug!(
4436                    message_id = %message.id,
4437                    "At the forwarding limit; not handing this to further neighbors"
4438                );
4439                break;
4440            }
4441
4442            let deposit;
4443            let frame: &Message = match custody_class {
4444                Some(class) if !self.custody_suppressed_toward(&message.id, &target) => {
4445                    let mut stamped = message.clone();
4446                    stamped
4447                        .metadata
4448                        .insert(CUSTODY_META_KEY.to_string(), class.to_string());
4449                    deposit = stamped;
4450                    &deposit
4451                }
4452                _ => message,
4453            };
4454
4455            match self.transport_manager.send_to_neighbor(&target, frame) {
4456                Ok(transport) => {
4457                    handed_to += 1;
4458                    debug!(
4459                        message_id = %message.id,
4460                        recipient = %message.recipient,
4461                        next_hop = %target,
4462                        transport = ?transport,
4463                        deposit = frame.metadata.contains_key(CUSTODY_META_KEY),
4464                        "Handed message to a neighbor to carry"
4465                    );
4466                }
4467                Err(err) => {
4468                    debug!(
4469                        message_id = %message.id,
4470                        next_hop = %target,
4471                        error = %err,
4472                        "Neighbor could not take the message"
4473                    );
4474                }
4475            }
4476        }
4477
4478        handed_to
4479    }
4480
4481    /// Puts a session-handshake frame (a key package or a Welcome) on its
4482    /// way, returning how many neighbours took a copy.
4483    ///
4484    /// Invariant: a handshake frame for a peer that no carrier is known to
4485    /// reach is handed to the mesh, whatever the direct send returned. A
4486    /// carrier's `Ok` is not that knowledge. The internet transport enqueues
4487    /// for any recipient, the one the relay has already said it cannot reach
4488    /// included, and Reticulum accepts for routing. Offering only when the
4489    /// direct send failed left a device with either carrier up unable to start
4490    /// a session with a peer only the mesh reaches: the carrier took every key
4491    /// package and Welcome and delivered none, and the message waiting on the
4492    /// session never left. This is the question
4493    /// [`Self::handle_send_success`] asks of every ordinary message, asked of
4494    /// the two frames that do not go through it.
4495    ///
4496    /// Additive, as there: a copy that also arrives through the carrier is a
4497    /// duplicate the receiver already absorbs. `Err` only when no carrier
4498    /// accepted the frame and no neighbour took it.
4499    pub(super) fn send_or_carry_handshake(&mut self, message: &Message) -> Result<usize> {
4500        let direct = self.transport_manager.send(message);
4501        let handed_to_mesh = if self.needs_mesh_copy(message.recipient.as_str(), direct.is_ok()) {
4502            self.offer_to_mesh(message)
4503        } else {
4504            0
4505        };
4506        if handed_to_mesh > 0 {
4507            debug!(
4508                recipient = %message.recipient,
4509                message_id = %message.id,
4510                handed_to_mesh,
4511                carrier_accepted = direct.is_ok(),
4512                "Handshake frame handed to neighbours to carry"
4513            );
4514        }
4515        match direct {
4516            Ok(()) => Ok(handed_to_mesh),
4517            Err(_) if handed_to_mesh > 0 => Ok(handed_to_mesh),
4518            Err(err) => Err(err),
4519        }
4520    }
4521
4522    /// Confirms that a message was successfully sent by the transport layer.
4523    pub fn on_transport_send_confirmed(&mut self, message_id: &str) -> Result<()> {
4524        let Some(peer_id) = self.find_welcome_peer_by_message_id(message_id) else {
4525            return Ok(());
4526        };
4527
4528        let updated = self
4529            .welcome_lifecycles
4530            .get(&peer_id)
4531            .cloned()
4532            .ok_or_else(|| Error::Other(format!("Missing welcome lifecycle for {}", peer_id)))?;
4533
4534        if matches!(
4535            updated.state,
4536            WelcomeDeliveryState::Sent | WelcomeDeliveryState::Expired
4537        ) {
4538            return Ok(());
4539        }
4540
4541        // A relay DeliveryError verdict outranks a late wire confirm: the
4542        // socket write succeeded, but the relay saw the frame and dropped it
4543        // (recipient offline). When the verdict raced ahead of this confirm,
4544        // the record is already parked `Failed`/`PeerUnreachable`; letting
4545        // the confirm proceed would resurrect the false `Sent` the verdict
4546        // just corrected — and emit welcome_send_succeeded *after* the
4547        // corrective welcome_send_failed. Any legitimate new send attempt
4548        // moves the record to `SendAttempted` first, so `Failed` here means
4549        // this confirm belongs to the very send the relay already failed.
4550        if updated.state == WelcomeDeliveryState::Failed
4551            && updated.last_reason_code == Some(crate::events::WelcomeReasonCode::PeerUnreachable)
4552        {
4553            return Ok(());
4554        }
4555
4556        // A wire confirm for a reachability probe must not upgrade the
4557        // record to `Sent`. With a live unreachable-park counter the send in
4558        // flight IS the escalating probe — every reachability edge clears the
4559        // counter before sending anything else — and the socket write
4560        // succeeding adds nothing the park didn't already know. Whenever the
4561        // relay's push fallback succeeds it returns no `DeliveryError` at
4562        // all, so a probe marked `Sent` here would go quiet forever:
4563        // `next_retry_at` cleared below, `Sent` outside every retry scan, and
4564        // `rearm_welcome_for_peer` a no-op on it — the exact edge-only dead
4565        // end the probe exists to close, reachable only through this bridge
4566        // confirm (mesh carriers never wire-confirm). Leaving the record
4567        // `SendAttempted` keeps the confirm deadline armed: a relay verdict
4568        // re-parks it, and an accepted-but-unanswered frame resolves at the
4569        // confirm timeout, which re-parks via
4570        // `welcome_probe_repark_permitted`. Genuine delivery still converges
4571        // regardless — the peer proving the session marks the welcome `Sent`
4572        // through `confirm_session_state`. This also withholds the
4573        // `welcome_send_succeeded` a probe's socket write would otherwise
4574        // emit, which the next verdict or timeout would only have to correct.
4575        if matches!(updated.state, WelcomeDeliveryState::SendAttempted)
4576            && updated.unreachable_parks > 0
4577        {
4578            return Ok(());
4579        }
4580
4581        {
4582            let record = self.welcome_lifecycles.get_mut(&peer_id).ok_or_else(|| {
4583                Error::Other(format!("Missing welcome lifecycle for {}", peer_id))
4584            })?;
4585            record.next_retry_at = None;
4586            record.last_reason_code = None;
4587            record.last_transport_error = None;
4588        }
4589        self.transition_welcome_state(&peer_id, WelcomeDeliveryState::Sent, "transport_confirmed")?;
4590
4591        let sent_snapshot = self
4592            .welcome_lifecycles
4593            .get(&peer_id)
4594            .cloned()
4595            .ok_or_else(|| Error::Other(format!("Missing welcome lifecycle for {}", peer_id)))?;
4596        if let Ok(state) = lock_shared_state(&self.shared_state) {
4597            state.emit_event(Event::welcome_send_succeeded(
4598                peer_id.clone(),
4599                sent_snapshot.welcome_message.id.as_str().to_string(),
4600                sent_snapshot.group_id,
4601                sent_snapshot.attempt,
4602            ));
4603        }
4604
4605        // The welcome is now confirmed as sent. Send a confirmation probe
4606        // immediately so the session can be confirmed without waiting for the
4607        // next process() tick — an optimistic fast-path for the common case
4608        // where the transport confirms promptly.
4609        self.send_session_confirmation_probe(&peer_id, "transport_confirmed");
4610
4611        Ok(())
4612    }
4613
4614    /// Handles asynchronous transport send failures for pending welcome
4615    /// sends and outbound connection requests.
4616    pub fn on_transport_send_failed(
4617        &mut self,
4618        message_id: &str,
4619        transport_error: Option<String>,
4620    ) -> Result<()> {
4621        self.on_transport_send_failed_via(message_id, transport_error, None)
4622    }
4623
4624    /// [`Self::on_transport_send_failed`] with the carrier that reported it.
4625    ///
4626    /// The carrier is what turns a failure into a *fact*: "the recipient is
4627    /// not reachable" is meaningless on its own, since a device may hold
4628    /// several carriers and only one of them is making the claim. Bridges that
4629    /// know which transport answered pass it; the plain form above keeps
4630    /// working for callers that do not, and records no fact rather than
4631    /// guessing one.
4632    pub fn on_transport_send_failed_via(
4633        &mut self,
4634        message_id: &str,
4635        transport_error: Option<String>,
4636        carrier: Option<TransportType>,
4637    ) -> Result<()> {
4638        // Classified at the boundary, once, and never carried past it.
4639        //
4640        // The platform bridges build this string from the relay's own
4641        // `DeliveryError.reason` (`recipient_unreachable: <relay text>`), and
4642        // the Nostr bridge from a relay's `OK` rejection text — remote-chosen,
4643        // unbounded, and downstream of here it reaches three event fields the
4644        // telemetry scrubber ships verbatim, each beside a recipient it
4645        // hashes. Even the honest relay renders the peer into its wording.
4646        //
4647        // The token itself is load-bearing and survives: the two prefix tests
4648        // below and the DM park key on `recipient_unreachable`, so classifying
4649        // to the bare token keeps every protocol decision intact and drops
4650        // only the prose. The raw string stays in this one device log.
4651        if let Some(raw) = transport_error.as_deref() {
4652            debug!(
4653                message_id = %message_id,
4654                transport_error = %raw.chars().take(256).collect::<String>(),
4655                "Platform reported a send failure"
4656            );
4657        }
4658        let transport_error = transport_error
4659            .as_deref()
4660            .map(classify_transport_send_error);
4661        // Whether the relay's mailbox holds this frame is orthogonal to why
4662        // the socket write missed, and it changes exactly one thing: a parked
4663        // plain DM needs no reachability probe, because the relay re-sends the
4664        // held copy on the recipient's next connection. So it is lifted out
4665        // here and the token normalized onto the vocabulary the rest of this
4666        // function already speaks — which is what leaves connection requests
4667        // and Welcomes on the paths they are on today.
4668        let stored = matches!(
4669            transport_error,
4670            Some(SEND_FAIL_REASON_RELAY_STORED | SEND_FAIL_REASON_RELAY_PUSHED_STORED)
4671        );
4672        let transport_error = match transport_error {
4673            Some(SEND_FAIL_REASON_RELAY_STORED) => Some(SEND_FAIL_REASON_RECIPIENT_UNREACHABLE),
4674            Some(SEND_FAIL_REASON_RELAY_PUSHED_STORED) => Some(SEND_FAIL_REASON_RELAY_PUSHED),
4675            other => other,
4676        };
4677        // A relay push is not a failure, so it is handled before either typed
4678        // branch below can read it as one: a connection request would be
4679        // fast-failed as undeliverable and a Welcome moved to `Failed`, both
4680        // for a frame the push may have delivered.
4681        if transport_error == Some(SEND_FAIL_REASON_RELAY_PUSHED) {
4682            self.park_relay_pushed_dm(message_id, carrier, stored);
4683            return Ok(());
4684        }
4685        // Connection requests first: the relay's "recipient offline"
4686        // DeliveryError is the only fast, authoritative failure signal a
4687        // request gets (there is no ACK from an offline peer — the app
4688        // would otherwise wait out the full retry budget for a generic
4689        // message_failed). Non-unreachable errors stay with the generic
4690        // retry machinery: the request is queued/retried and may still
4691        // deliver.
4692        if let Some(pending) = self.pending_connection_requests.get(message_id) {
4693            // The TTL must hold at read time too: the insert-path prune only
4694            // runs on the next `send_connection_request`, so an idle map can
4695            // hold an entry far past the correlation window — a failure
4696            // signal that stale belongs to a request the app has long
4697            // stopped waiting on and must not fire the event.
4698            if pending.sent_at.elapsed() > PENDING_CONNECTION_REQUEST_TTL {
4699                self.pending_connection_requests.remove(message_id);
4700            } else {
4701                let unreachable = transport_error == Some(SEND_FAIL_REASON_RECIPIENT_UNREACHABLE);
4702                if unreachable {
4703                    let recipient = pending.recipient.clone();
4704                    self.pending_connection_requests.remove(message_id);
4705                    warn!(
4706                        recipient = %recipient,
4707                        message_id = %message_id,
4708                        "Connection request undeliverable: recipient unreachable"
4709                    );
4710                    if let Ok(state) = lock_shared_state(&self.shared_state) {
4711                        state.emit_event(Event::connection_request_undeliverable(
4712                            recipient,
4713                            message_id.to_string(),
4714                            transport_error.unwrap_or(SEND_FAIL_REASON_TRANSPORT),
4715                        ));
4716                    }
4717                    return Ok(());
4718                }
4719            }
4720        }
4721
4722        let Some(peer_id) = self.find_welcome_peer_by_message_id(message_id) else {
4723            // Not a connection request, not a welcome: a plain DM or media
4724            // chunk. Surface the relay's verdict and park plain DMs so the
4725            // ACK retry budget stops burning against an offline peer.
4726            self.handle_recipient_unreachable_for_message(
4727                message_id,
4728                transport_error,
4729                carrier,
4730                stored,
4731            );
4732            return Ok(());
4733        };
4734        // A reason tagged "recipient_unreachable" (the internet bridge's
4735        // translation of the relay's DeliveryError) is authoritative proof
4736        // the frame was dropped: the carrier is up but *this peer* is not on
4737        // it. It must be handled even for records already wire-confirmed
4738        // (`Sent`) — the bridge confirms on socket-write success, before the
4739        // relay can answer, so the DeliveryError normally arrives when the
4740        // record is already Sent and the plain failure path below would
4741        // no-op on it, stranding a false `Sent`.
4742        let peer_unreachable = transport_error == Some(SEND_FAIL_REASON_RECIPIENT_UNREACHABLE);
4743        if peer_unreachable {
4744            if let Some(carrier) = carrier {
4745                self.reachability.record(
4746                    &peer_id,
4747                    carrier,
4748                    Claim::Unreachable,
4749                    FactSource::GatewayVerdict,
4750                    Instant::now(),
4751                );
4752            }
4753            // A held verdict that finds its Welcome already `Failed` is the
4754            // second report of one send, not a second failure. The bridges
4755            // report the id a `stored` verdict names even after that
4756            // recipient's in-flight set was drained, because for a plain DM
4757            // the second report is what retires the probe the first one
4758            // scheduled. So a Welcome anywhere but first in a burst is failed
4759            // by the drain as `recipient_unreachable`, then reported again as
4760            // `relay_stored` by its own verdict. Applied twice, the failure
4761            // climbs two rungs of the Welcome ladder for one send and tells
4762            // the app `welcome_send_failed` twice.
4763            //
4764            // `Failed` is what identifies the repeat: a resend moves the record
4765            // to `SendAttempted` before it writes, so the verdict for a fresh
4766            // send never finds it there. Holding the frame changes nothing for
4767            // a Welcome, which keeps its own ladder either way, so skipping the
4768            // repeat loses nothing. The fact above is still recorded, because
4769            // the relay did say it again.
4770            if stored
4771                && self
4772                    .welcome_lifecycles
4773                    .get(&peer_id)
4774                    .is_some_and(|record| record.state == WelcomeDeliveryState::Failed)
4775            {
4776                debug!(
4777                    peer_id = %peer_id,
4778                    message_id = %message_id,
4779                    "Held verdict for a Welcome this send already failed; not failing it twice"
4780                );
4781                return Ok(());
4782            }
4783            return self.apply_recipient_unreachable_failure(&peer_id, transport_error);
4784        }
4785        let reason = crate::events::WelcomeReasonCode::TransportUnavailable;
4786        // No raw error is available on this async path, so infer no-carrier
4787        // from live connectivity: with no transport currently available the
4788        // peer is simply unreachable and the Welcome must be kept alive, not
4789        // aged.
4790        let no_carrier = self.transport_manager.get_available_transports().is_empty();
4791        let _ = self.apply_welcome_send_failure(
4792            &peer_id,
4793            reason,
4794            transport_error,
4795            no_carrier,
4796            "transport_failed",
4797        )?;
4798        Ok(())
4799    }
4800
4801    /// Handles a relay `recipient_unreachable` verdict for a plain DM or
4802    /// media chunk: emits the non-terminal [`Event::MessageUndeliverable`],
4803    /// then *parks* plain DMs.
4804    ///
4805    /// Parking drops the pending ACK and the retry-queue entry while the
4806    /// outbox entry stays put — exactly the "stranded" state every re-drive
4807    /// edge already flushes (`flush_outbox_all` on reconnect/start,
4808    /// `flush_outbox_for_peer_via` on discovery and presence-online, the latter
4809    /// fed by [`Self::presence_watch_peers`]). Without the park, the offline
4810    /// peer's missing ACK burns the full `max_retries` budget in minutes
4811    /// (`process_timed_out_acks`) and settles a 7-day outbox message
4812    /// terminally; a re-driven send re-registers a fresh ACK, so the budget
4813    /// normally burns only against a peer believed reachable. The exception
4814    /// is the reachability probe below: its send re-enters the ACK machinery
4815    /// but may never earn a relay verdict (a mesh carrier cannot produce one
4816    /// at all), so exhaustion with a live park counter *re-parks* instead of
4817    /// settling ([`Self::try_repark_exhausted_dm`]). Settlement is reserved
4818    /// for delivery or outbox-lifetime expiry.
4819    ///
4820    /// The park always keeps a timed reachability probe whose interval
4821    /// escalates per consecutive park (15s → 600s cap, per-peer counter reset
4822    /// on any reachability edge). It is deliberately carrier-agnostic: with a
4823    /// local mesh carrier the peer may be a room away — possibly already a
4824    /// discovered neighbor, so no future edge would fire for it — and on an
4825    /// internet-only device the external edges (presence rotation, reconnect)
4826    /// are the *only* other recovery, which leaves delivery hostage to a
4827    /// bridge's polling cadence. See [`Self::park_unreachable_dm`] for why
4828    /// probing over the relay is self-limiting.
4829    ///
4830    /// Media chunks are never parked: their offline story is retry
4831    /// exhaustion → transfer abort → persisted descriptor →
4832    /// `MediaResendRequired` with app-resupplied bytes. Messages without an
4833    /// outbox entry (nothing to attribute a recipient from) are skipped.
4834    fn handle_recipient_unreachable_for_message(
4835        &mut self,
4836        message_id: &str,
4837        transport_error: Option<&'static str>,
4838        carrier: Option<TransportType>,
4839        stored: bool,
4840    ) {
4841        let Some(reason) = transport_error.filter(|r| *r == SEND_FAIL_REASON_RECIPIENT_UNREACHABLE)
4842        else {
4843            return;
4844        };
4845        let Ok(parsed_id) = MessageId::from_str(message_id) else {
4846            return;
4847        };
4848        let (entry, is_media) = match self.outbox.get(&parsed_id) {
4849            Some(entry) => (entry, false),
4850            None => match self.media_outbox.get(&parsed_id) {
4851                Some(entry) => (entry, true),
4852                None => {
4853                    if !self.note_superseded_probe_verdict(&parsed_id, carrier) {
4854                        debug!(
4855                            message_id = %message_id,
4856                            "Unreachable verdict for message without outbox entry, dropping"
4857                        );
4858                    }
4859                    return;
4860                }
4861            },
4862        };
4863        let recipient = entry.message.recipient.as_str().to_string();
4864        let attempt_count = entry.attempt_count;
4865        // A verdict for a frame the relay has already pushed is the answer to
4866        // our own probe (`OutboxEntry::relay_pushed`): the relay refuses to
4867        // notify twice, so it says `DeliveryError` for a message that may be
4868        // sitting delivered on the recipient's device. Everything below still
4869        // applies (the recipient is not on the relay, so record it, back the
4870        // probe off, and re-park), except the app-facing event: telling the
4871        // app the message is undeliverable would be exactly the claim
4872        // `park_relay_pushed_dm` exists not to make. Media is never pushed
4873        // and never parked, so the flag is only ever read off the main outbox.
4874        let relay_pushed = !is_media && entry.relay_pushed;
4875        // The verdict, recorded as a fact before anything acts on it. Parking
4876        // below reacts to *this* message; the fact is what lets the next send
4877        // to the same recipient skip a carrier that has already said no.
4878        if let Some(carrier) = carrier {
4879            self.reachability.record(
4880                &recipient,
4881                carrier,
4882                Claim::Unreachable,
4883                FactSource::GatewayVerdict,
4884                Instant::now(),
4885            );
4886        }
4887        // Media never parks, so the mesh offer the park performs for a DM has
4888        // to happen here instead. Cloned while the entry is still borrowed;
4889        // only on a verdict, so not a per-chunk cost.
4890        let media_frame = is_media.then(|| entry.message.clone());
4891        let file_id = self
4892            .outbound_media_chunks
4893            .get(&parsed_id)
4894            .map(|(file_id, _)| file_id.clone());
4895        // If this peer is also an unconfirmed-session probe target, fold the
4896        // verdict into the probe schedule so it backs off (15s -> 600s) instead
4897        // of re-probing every 5s forever. No-op for peers that are not probe
4898        // targets (e.g. an ordinary DM to a confirmed peer). Done here, after
4899        // the `entry` borrow of `self.outbox` has ended.
4900        self.note_confirmation_probe_unreachable(&recipient);
4901        if relay_pushed {
4902            debug!(
4903                message_id = %message_id,
4904                "Recipient unreachable for a relay-pushed DM: re-parking without notifying the app"
4905            );
4906        } else {
4907            warn!(
4908                message_id = %message_id,
4909                file_id = ?file_id,
4910                parked = !is_media,
4911                "Recipient unreachable for in-flight message (non-terminal)"
4912            );
4913            self.emit_event(Event::message_undeliverable(
4914                parsed_id.clone(),
4915                recipient.clone(),
4916                reason,
4917                file_id,
4918            ));
4919        }
4920        if let Some(frame) = media_frame {
4921            // The recipient is not on the relay, but they may be a few devices
4922            // away. A chunk keeps its pending ACK (media is never parked), so
4923            // an answer coming back over the mesh settles it through the
4924            // ordinary path — nothing else is needed here.
4925            let handed_to_mesh = self.offer_to_mesh(&frame);
4926            if handed_to_mesh > 0 {
4927                debug!(
4928                    message_id = %message_id,
4929                    recipient = %recipient,
4930                    handed_to_mesh,
4931                    "Relay cannot reach this chunk's recipient; handed it to neighbors"
4932                );
4933            }
4934            return;
4935        }
4936
4937        self.park_unreachable_dm(&parsed_id, &recipient, attempt_count, stored);
4938    }
4939
4940    /// Whether a platform send report also belongs in the carrier's delivery
4941    /// metrics (`Transport::report_send_failure`).
4942    ///
4943    /// Every report does except the two push tokens. The bridges hand the
4944    /// relay's `MessageSent { pushed: true }` to
4945    /// [`Self::on_transport_send_failed_via`] because that is their one call
4946    /// for "the relay answered about this id", but the relay took the frame
4947    /// into a device push, which is not a failed send. A wrapper that scores
4948    /// the report without asking here records a failure whenever the relay's
4949    /// answer beats the bridge's own write confirmation, against the carrier
4950    /// the router ranks by that ratio.
4951    ///
4952    /// `relay_stored` is not in that company: it answers a `DeliveryError`,
4953    /// where the write really did miss. That the mailbox kept a copy says
4954    /// nothing about the carrier.
4955    pub fn send_report_is_carrier_failure(reason: Option<&str>) -> bool {
4956        !matches!(
4957            reason.map(classify_transport_send_error),
4958            Some(SEND_FAIL_REASON_RELAY_PUSHED | SEND_FAIL_REASON_RELAY_PUSHED_STORED)
4959        )
4960    }
4961
4962    /// Handles the relay's `MessageSent { pushed: true }` answer: the recipient
4963    /// has no live socket, and the frame went out in a push notification.
4964    ///
4965    /// A plain DM is parked exactly as for a `DeliveryError`: without the park
4966    /// the missing ACK burns the retry budget to a terminal `message_failed`.
4967    /// If the push did deliver, the recipient's acknowledgement settles the
4968    /// parked entry (`settle_dm_without_pending_ack`).
4969    ///
4970    /// `stored` is the relay saying its mailbox also holds the frame
4971    /// (`MessageSent { pushed: true, stored: true }`, reported as
4972    /// [`SEND_FAIL_REASON_RELAY_PUSHED_STORED`]). It reaches
4973    /// [`Self::park_unreachable_dm`] and nothing else: the park is identical
4974    /// but for its recovery, which becomes the relay's own redelivery instead
4975    /// of a timed probe. Without it — an older relay, a mailbox turned off, a
4976    /// store that failed — the probe stands, because then a lost push really
4977    /// does mean nothing re-delivers the frame when the recipient reconnects.
4978    ///
4979    /// Two differences from the `DeliveryError` path, both because the push
4980    /// may have delivered the frame:
4981    ///
4982    /// - **Plain DMs only.** A connection request keeps its typed tracking and
4983    ///   a Welcome its lifecycle, both awaiting the acknowledgement or session
4984    ///   confirmation a delivered push produces. `is_parkable_plain_dm`
4985    ///   excludes both, and media chunks are never in `outbox`.
4986    /// - **No `MessageUndeliverable`, now or on any later probe.** The app
4987    ///   would be told a message is undeliverable when it may already have
4988    ///   arrived. The park's timed probe resends the same id, and the relay
4989    ///   answers a retry of a pushed message with `DeliveryError` rather than
4990    ///   a second notification, which the bridges can only report as
4991    ///   `recipient_unreachable`. So the entry is flagged
4992    ///   (`OutboxEntry::relay_pushed`, persisted) and
4993    ///   [`Self::handle_recipient_unreachable_for_message`] re-parks a flagged
4994    ///   entry silently. Without the flag the guarantee would last exactly
4995    ///   one probe interval.
4996    ///
4997    /// The flag also bounds the entry: it is dropped, silently and for the
4998    /// same reason, once [`offline_protocol_reliability::constants::REDELIVERY_WINDOW_MS`]
4999    /// has passed since `first_sent_at`, in process (`cleanup_outbox`) and at
5000    /// restore alike. Neither the sliding lifetime nor the restore refresh
5001    /// extends it — a re-send past the relay's push-claim TTL is a second
5002    /// visible notification, and past the recipient's dedup window it is a
5003    /// replay that fails to decrypt and is never acknowledged.
5004    fn park_relay_pushed_dm(
5005        &mut self,
5006        message_id: &str,
5007        carrier: Option<TransportType>,
5008        stored: bool,
5009    ) {
5010        let Ok(parsed_id) = MessageId::from_str(message_id) else {
5011            return;
5012        };
5013        if !self.is_parkable_plain_dm(&parsed_id) {
5014            self.note_superseded_probe_verdict(&parsed_id, carrier);
5015            return;
5016        }
5017        let Some(entry) = self.outbox.get_mut(&parsed_id) else {
5018            return;
5019        };
5020        let recipient = entry.message.recipient.as_str().to_string();
5021        let attempt_count = entry.attempt_count;
5022        let newly_pushed = !entry.relay_pushed;
5023        entry.relay_pushed = true;
5024        if newly_pushed {
5025            // Re-persisted on the flip only: the entry was written when it
5026            // was created, and the flag has to survive a restart for the
5027            // silent re-park to hold on the first probe after one.
5028            if let Some(entry) = self.outbox.get(&parsed_id) {
5029                self.persist_outbox_entry(entry);
5030            }
5031        }
5032        // The relay has said this recipient is not on it, which is the same
5033        // fact a `DeliveryError` records, and the same thing the probe backoff
5034        // keys on.
5035        if let Some(carrier) = carrier {
5036            self.reachability.record(
5037                &recipient,
5038                carrier,
5039                Claim::Unreachable,
5040                FactSource::GatewayVerdict,
5041                Instant::now(),
5042            );
5043        }
5044        self.note_confirmation_probe_unreachable(&recipient);
5045        debug!(
5046            message_id = %message_id,
5047            stored = stored,
5048            "Relay pushed a DM to an offline recipient; parking it"
5049        );
5050        self.park_unreachable_dm(&parsed_id, &recipient, attempt_count, stored);
5051    }
5052
5053    /// The park action shared by the relay-verdict path
5054    /// ([`Self::handle_recipient_unreachable_for_message`]) and the
5055    /// exhausted-probe path ([`Self::try_repark_exhausted_dm`]): drops the
5056    /// pending ACK and any scheduled retry so nothing burns budget against
5057    /// the offline peer, offers the frame to the mesh, then schedules the
5058    /// escalating reachability probe.
5059    ///
5060    /// `stored` drops that last step. The relay's mailbox is holding the
5061    /// frame and will deliver it on the recipient's next connection, so the
5062    /// park keeps everything that stops budget burning and adds no traffic of
5063    /// its own; recovery is the relay's drain, with the reachability edge
5064    /// behind it as for every parked frame. Everything the probe rationale
5065    /// below argues for still holds for an unheld frame.
5066    ///
5067    /// Two things that rationale does *not* cover, both deliberate:
5068    ///
5069    /// - **What is actually saved is the rungs, not the re-drive.** The edge
5070    ///   fires on any reconnect, not only on an eviction, and
5071    ///   [`Self::flush_outbox_for_peer_via`] selects on "in the outbox, not
5072    ///   awaiting an ACK" — so a returning peer re-drives a held frame
5073    ///   alongside the relay's own drain, exactly as it would an unheld one.
5074    ///   Dedup collapses the pair. What the flag removes is the 15s→600s
5075    ///   ladder *while the peer is away*, which is the traffic the relay asks
5076    ///   senders not to generate. It follows that past the mailbox's own
5077    ///   retention the delivery guarantee is that edge and the outbox
5078    ///   lifetime — the same trade `edge_driven_unreachable_dm` opts into for
5079    ///   every parked frame, taken unconditionally here because the mailbox is
5080    ///   already committed to the redelivery.
5081    /// - **`stored` is not persisted on the entry, unlike
5082    ///   `OutboxEntry::relay_pushed`.** Nothing needs it to be: a restart
5083    ///   re-drives the outbox, the relay answers `stored` again for a frame it
5084    ///   still holds, and the park is re-derived from that answer — whereas
5085    ///   `relay_pushed` has to survive, because it suppresses an app-facing
5086    ///   event no later answer would restate. Persisting this one would also
5087    ///   be wrong on its own terms: it records what the relay's mailbox held
5088    ///   at park time, and the mailbox can evict between then and the next
5089    ///   launch, so a restored flag would suppress the probe on the strength
5090    ///   of a copy that may be gone.
5091    ///
5092    /// The mesh offer is here rather than at either caller so both parks make
5093    /// it, and because this is where the frame is already at hand. Its rationale
5094    /// is inline below; the short version is that a relay verdict is the only
5095    /// per-peer reachability fact this device ever receives, and it says the
5096    /// recipient is somewhere the relay cannot go.
5097    ///
5098    /// The probe is carrier-agnostic — DORS picks its transport like any
5099    /// other send — because an internet-only device is the common
5100    /// configuration, not an exotic one, and parking it edge-only leaves the
5101    /// message with *no* self-recovery at all: no pending ACK to time out, no
5102    /// retry entry, nothing on a timer. Recovery then depends entirely on an
5103    /// external reachability edge (`flush_outbox_all` on reconnect/start, or
5104    /// a presence-online answer), i.e. on a bridge's presence-polling cadence
5105    /// — tens of seconds to minutes, and nothing at all for a headless
5106    /// consumer that never polls presence.
5107    ///
5108    /// Probing over the relay is self-limiting in every outcome, which is why
5109    /// no carrier guard is needed:
5110    /// - the peer is still offline and the relay's push fallback fails → a
5111    ///   fresh `DeliveryError` re-enters this park, escalating the interval
5112    ///   (15s → 600s cap) and re-emitting the non-terminal
5113    ///   [`Event::MessageUndeliverable`];
5114    /// - the relay's push fallback succeeds → the relay answers
5115    ///   `MessageSent { pushed: true }`, which parks the message again
5116    ///   ([`Self::park_relay_pushed_dm`]) and escalates the interval. That
5117    ///   happens once per push: the relay remembers which
5118    ///   `(sender, recipient, message_id)` triples it has pushed, for 7 days
5119    ///   (one day on older relays), and answers a repeat inside that window
5120    ///   with `DeliveryError`
5121    ///   (`already_pushed`) instead of a second notification. The outbox id
5122    ///   is stable across every probe, so each later rung earns that
5123    ///   `DeliveryError`, which re-parks the entry silently
5124    ///   (`OutboxEntry::relay_pushed`) rather than re-emitting
5125    ///   [`Event::MessageUndeliverable`] for a frame the push may have
5126    ///   delivered;
5127    /// - the peer is back → the probe *is* the delivery, which beats waiting
5128    ///   for any presence edge.
5129    ///
5130    /// The counter is per-*peer* while the probes are per-*message*: a burst
5131    /// of N DMs to one offline peer escalates the shared ladder once per
5132    /// park, so no individual message climbs 15s → 600s — the Nth already
5133    /// starts high on it. The compensating edge is delivery: an ACK for any
5134    /// one of the peer's DMs re-drives the rest immediately
5135    /// ([`Self::handle_ack_message`]) instead of leaving them on their
5136    /// escalated timers.
5137    ///
5138    /// Relay traffic is bounded differently in each of those branches, and
5139    /// the difference matters for capacity planning now that the whole
5140    /// internet-only fleet is on this path:
5141    /// - verdict branch: the escalation is the bound — one frame per interval
5142    ///   per parked *message*, settling at one per 600s;
5143    /// - accepted branch: the probe registers a fresh ACK, and
5144    ///   [`crate::protocol::OfflineProtocol::process_retry_queue`] seeds that
5145    ///   ACK with the retry-queue entry's carried count (`retry_count + 1`)
5146    ///   rather than letting it restart at 0. A probe whose entry is fresh (a
5147    ///   reachability edge re-drove it with a new budget) still walks the full
5148    ///   ladder — up to `max_retries` sends, 1s → 300s backoff, ~800s
5149    ///   cumulative on the defaults — but a probe whose entry already carries
5150    ///   backoff resumes near its current position and re-parks after roughly
5151    ///   one more timeout instead of replaying the 1s floor. Plan relay
5152    ///   capacity against the fresh-entry number; the carried case only sends
5153    ///   less. Either way `try_repark_exhausted_dm` re-parks it at the
5154    ///   escalated interval. The carry-forward is load-bearing: without it a
5155    ///   never-ACKing (offline) recipient's probe pins `delay_for_retry` at
5156    ///   its 1s floor and floods the relay ~once per second.
5157    ///
5158    /// The outbox lifetime bounds the entry itself — and note the probe
5159    /// refreshes `last_sent_at` on every send, so the sliding 7-day window
5160    /// stops binding and settlement moves out to the absolute cap
5161    /// ([`crate::constants::OUTBOX_ABSOLUTE_LIFETIME_FACTOR`] × the lifetime),
5162    /// or, for a relay-pushed entry, to its fixed redelivery window (see
5163    /// [`Self::park_relay_pushed_dm`]).
5164    /// Note also that a probe can be routed onto the relay when a mesh
5165    /// carrier is up — DORS has always been free to choose it — so this path
5166    /// is not new behavior for the relay, only newly reachable on
5167    /// internet-only devices.
5168    ///
5169    /// The counter increment is unconditional for the same reason: it is what
5170    /// arms [`Self::try_repark_exhausted_dm`], so a probe that exhausts its
5171    /// ACK budget re-parks instead of settling terminally.
5172    fn park_unreachable_dm(
5173        &mut self,
5174        message_id: &MessageId,
5175        recipient: &str,
5176        attempt_count: u32,
5177        stored: bool,
5178    ) {
5179        // Park: no pending ACK to time out, no backoff-scheduled resend.
5180        self.ack_manager.remove_ack(message_id);
5181        self.retry_queue.remove(&message_id.as_str());
5182
5183        let parks = {
5184            let count = self
5185                .dm_unreachable_parks
5186                .entry(recipient.to_string())
5187                .or_insert(0);
5188            *count = count.saturating_add(1);
5189            *count
5190        };
5191        // parks >= 1 here; shift clamped so 15 << 6 = 960 is the largest
5192        // pre-cap value — no overflow risk.
5193        let retry_in_secs = (WELCOME_NO_CARRIER_RETRY_SECS << (parks - 1).min(6))
5194            .min(WELCOME_UNREACHABLE_RETRY_CAP_SECS);
5195        let mut handed_to_mesh = 0usize;
5196        if let Some(entry) = self.outbox.get(message_id) {
5197            let message = entry.message.clone();
5198            // Ask the neighbors. The relay has just told us something no local
5199            // transport status can: that *this peer* is not on it. That is the
5200            // peer-specific fact `can_reach_without_carrying` cannot supply —
5201            // it answers yes for every recipient while an infrastructure
5202            // carrier is up, which is why the send-time offer never fires on an
5203            // online device and why the whole mixed-neighborhood case (an
5204            // online sender, a recipient reachable only across the mesh) used
5205            // to wait here for a peer that was never going to come online.
5206            //
5207            // Additive, exactly like the send-time offer: the outbox entry and
5208            // the probe below stand either way, since a neighbor taking a copy
5209            // is not proof of arrival. What settles it is the acknowledgement
5210            // coming back — over the mesh, for a message the relay could not
5211            // deliver, which is why parked DMs must be settleable without a
5212            // pending ACK (`settle_dm_without_pending_ack`). Without that arm this
5213            // offer would deliver messages the sender never learns about.
5214            //
5215            // Re-offering on every park is bounded by construction: the
5216            // escalation (15s → 600s) bounds how often we ask, each offer
5217            // spends the same own-send tokens as any other frame of ours, a
5218            // neighbor that already carried the id takes our re-send again
5219            // only within its per-neighbor allowance for us, and every device
5220            // further on suppresses the id for the whole `RelaySeenCache`
5221            // retention window.
5222            handed_to_mesh = self.offer_to_mesh(&message);
5223            // Default: keep the documented perpetual timed probe (15s->600s cap
5224            // on every carrier). Opt-in (`edge_driven_unreachable_dm`): after a
5225            // bounded number of probes, stop the timer and leave the message in
5226            // the outbox edge-driven — re-driven only when the peer next proves
5227            // reachable (inbound frame / presence-online -> on_neighbor_discovered
5228            // -> flush_outbox_for_peer_via). This zeroes steady-state relay
5229            // traffic to a durably-gone peer, at the cost of the timed-probe
5230            // self-recovery guarantee for a silent returning peer.
5231            // `stored`: the relay's mailbox holds this frame and re-sends it
5232            // on the recipient's next connection, so the probe has nothing
5233            // left to achieve — it would re-store the same row, once per rung,
5234            // for as long as the peer stays away. Edge-driven unconditionally
5235            // here, not behind `edge_driven_unreachable_dm`: that opt-in
5236            // trades away a delivery guarantee, while this is the one case
5237            // where something else already carries it.
5238            let edge_driven = stored
5239                || (self.config.reliability.retry.edge_driven_unreachable_dm
5240                    && parks > crate::protocol::types::DM_UNREACHABLE_PROBE_LIMIT);
5241            if !edge_driven {
5242                let _ = self.retry_queue.enqueue_with_delay(
5243                    message,
5244                    attempt_count,
5245                    (retry_in_secs * 1000) as u64,
5246                );
5247            }
5248        }
5249        debug!(
5250            message_id = %message_id,
5251            recipient = %recipient,
5252            retry_in_secs = retry_in_secs,
5253            parks = parks,
5254            handed_to_mesh,
5255            stored = stored,
5256            "Parked unreachable DM"
5257        );
5258    }
5259
5260    /// Re-parks a plain DM whose ACK retry budget just exhausted while its
5261    /// recipient still holds a live unreachable-park counter, returning
5262    /// `true` when the caller (`handle_max_retries_exceeded`) must skip
5263    /// terminal settlement.
5264    ///
5265    /// A live counter means the relay declared the peer offline and no
5266    /// reachability edge has fired since (every edge clears it), so the
5267    /// exhausted budget was burnt by reachability probes — sends that succeed
5268    /// locally without proving the peer is back — not by a peer believed
5269    /// reachable. Over a mesh carrier such a probe can never earn a relay
5270    /// verdict at all; over the relay a verdict normally re-parks the message
5271    /// (resetting its ACK budget) well before exhaustion, so reaching here on
5272    /// an internet-only device means the probes were accepted for delivery
5273    /// but never answered. Settling terminally here would
5274    /// reintroduce the ~15-minute `message_failed` the park exists to
5275    /// prevent; re-parking keeps settlement reserved for delivery or
5276    /// outbox-lifetime expiry (the escalation cap bounds probe traffic, the
5277    /// outbox lifetime + absolute cap bound the entry itself).
5278    ///
5279    /// Deliberately narrow: pending connection requests keep their typed
5280    /// terminal path, welcomes keep their own lifecycle, and media chunks
5281    /// (never in `outbox`, and never counted) keep retry exhaustion →
5282    /// transfer abort → `MediaResendRequired`.
5283    pub(super) fn try_repark_exhausted_dm(&mut self, message_id: &MessageId) -> bool {
5284        if !self.is_parkable_plain_dm(message_id) {
5285            return false;
5286        }
5287        let Some(entry) = self.outbox.get(message_id) else {
5288            return false;
5289        };
5290        let recipient = entry.message.recipient.as_str();
5291        if !self.dm_unreachable_parks.contains_key(recipient) {
5292            return false;
5293        }
5294        let recipient = recipient.to_string();
5295        let attempt_count = entry.attempt_count;
5296        debug!(
5297            message_id = %message_id,
5298            recipient = %recipient,
5299            "ACK budget exhausted by reachability probes; re-parking instead of settling"
5300        );
5301        // Not `stored`: a held DM is never probed, so it does not burn an ACK
5302        // budget and does not arrive here. Anything that does got here by
5303        // probing, and keeps the probe it was re-parked from.
5304        self.park_unreachable_dm(message_id, &recipient, attempt_count, false);
5305        true
5306    }
5307
5308    /// True when `message_id` is a plain DM eligible for the
5309    /// unreachable-park machinery: not a pending connection request (typed
5310    /// terminal path), not an MLS welcome (own lifecycle), and still present
5311    /// in the outbox. Media chunks live in `media_outbox`, so they never
5312    /// pass the outbox check.
5313    pub(super) fn is_parkable_plain_dm(&self, message_id: &MessageId) -> bool {
5314        if self
5315            .pending_connection_requests
5316            .contains_key(&message_id.as_str())
5317        {
5318            return false;
5319        }
5320        if self
5321            .find_welcome_peer_by_message_id(&message_id.as_str())
5322            .is_some()
5323        {
5324            return false;
5325        }
5326        self.outbox.contains_key(message_id)
5327    }
5328
5329    /// Cancels the in-flight probe state (pending ACK + scheduled retry) of
5330    /// every parkable plain DM addressed to `peer_id`, so a
5331    /// reachability-edge flush can re-drive them.
5332    ///
5333    /// Only acts when the peer holds a live unreachable-park counter: a
5334    /// live counter means the relay declared the peer offline and no edge
5335    /// has fired since, so a pending ACK on the peer's DMs belongs to a
5336    /// reachability probe — a send that locally succeeded but has not been
5337    /// answered — not to a delivery in good standing.
5338    /// Without this cancel, the flush edges' awaiting-ACK filters skip
5339    /// exactly the probing messages while still clearing the counter,
5340    /// stranding them on minutes-scale ACK backoff after the relay already
5341    /// reported the peer online — and, with the counter gone, exposing them
5342    /// to terminal settlement on exhaustion (`try_repark_exhausted_dm`
5343    /// finds nothing). Canceling an ACK that was genuinely about to arrive
5344    /// is safe: receivers dedupe and re-ACK duplicates.
5345    ///
5346    /// Does NOT clear the counter itself — the flush edge owns that reset,
5347    /// after this runs.
5348    pub(super) fn cancel_probe_state_for_parked_peer(&mut self, peer_id: &str) {
5349        if !self.dm_unreachable_parks.contains_key(peer_id) {
5350            return;
5351        }
5352        let probing: Vec<MessageId> = self
5353            .outbox
5354            .values()
5355            .filter(|entry| entry.message.recipient.as_str() == peer_id)
5356            .map(|entry| entry.message.id.clone())
5357            .collect();
5358        for message_id in probing {
5359            if !self.is_parkable_plain_dm(&message_id) {
5360                continue;
5361            }
5362            self.ack_manager.remove_ack(&message_id);
5363            self.retry_queue.remove(&message_id.as_str());
5364        }
5365    }
5366
5367    /// Classifies an outbound internet frame as a server-plane control op the
5368    /// platform bridge should translate to (or mirror as) a relay-native
5369    /// message instead of an opaque `SendMessage`.
5370    ///
5371    /// Returns `(op, payload)` where `payload` is the JSON after the prefix
5372    /// (empty string for payload-less ops). `None` means normal traffic —
5373    /// send verbatim.
5374    ///
5375    /// Ops and their bridge semantics:
5376    /// - `group_relay_register` / `group_relay_broadcast` — self-addressed
5377    ///   relay hints (`send_group_message` optimization); REPLACE with
5378    ///   relay-native `CreateGroup`+member deltas / `SendGroupMessage`. The
5379    ///   relay does not intercept content prefixes, so untranslated frames
5380    ///   would be echoed back uselessly.
5381    /// - `group_mls_leave` — TAP, don't replace: the per-member leave
5382    ///   notification must still be delivered verbatim, but the bridge also
5383    ///   sends one relay-native `LeaveGroup` so the relay's group registry
5384    ///   (which feeds invite links and broadcast fan-out) doesn't go stale.
5385    ///
5386    /// Connection ops (`__CONN_REQ__`/`__CONN_ACC__`/`__CONN_REJ__`/
5387    /// `__CONN_CAN__`) deliberately do NOT classify: they ship verbatim as
5388    /// opaque `SendMessage` frames so the Ed25519 control signature in the
5389    /// message metadata survives to the receiver's security gate. The former
5390    /// relay-native translation (`SendConnectionRequest` & co.) rebuilt them
5391    /// unsigned on the receiving bridge, which the gate now rejects
5392    /// unconditionally — control traffic must be signed. Verbatim
5393    /// also gains the relay's push-notification fallback for offline
5394    /// recipients, which the relay-native connection frames never had. The
5395    /// cost is that pre-SDK relay clients (which only speak the relay-native
5396    /// frames) no longer interoperate with connection ops — an accepted
5397    /// trade: the SDK's internet transport is the only supported relay
5398    /// client.
5399    ///
5400    /// Only frames *originated by this device* classify. The mesh relays
5401    /// third-party messages verbatim (`try_relay_message`). The relay-hint
5402    /// ops additionally require the self-addressed recipient (that is how
5403    /// the core marks them as hints rather than traffic), so they classify
5404    /// only when sender AND recipient are both this device.
5405    ///
5406    /// Self-origination is proven by `hop_count == 0`, not by the `sender`
5407    /// field: `sender` is an unauthenticated wire field, so a mesh peer
5408    /// could forge `sender == self` on a frame we then relay — but
5409    /// `try_relay_message` increments the hop before re-sending, while every
5410    /// locally-originated frame leaves `send_internal_message` at hop 0.
5411    /// (Defense in depth: the receive loop also drops inbound frames
5412    /// claiming our own origin before they can reach the relay path.)
5413    pub fn internet_control_op(&self, message: &Message) -> Option<(&'static str, String)> {
5414        if message.hop_count.value() != 0 {
5415            return None;
5416        }
5417        let content = message.content.as_str();
5418        if message.sender.as_str() == self.local_id {
5419            if let Some(payload) = content.strip_prefix(internal_prefixes::GROUP_MLS_LEAVE) {
5420                return Some(("group_mls_leave", payload.to_string()));
5421            }
5422        }
5423        if message.sender.as_str() == self.local_id && message.recipient.as_str() == self.local_id {
5424            if let Some(payload) = content.strip_prefix(internal_prefixes::GROUP_RELAY_REGISTER) {
5425                return Some(("group_relay_register", payload.to_string()));
5426            }
5427            if let Some(payload) = content.strip_prefix(internal_prefixes::GROUP_RELAY_BROADCAST) {
5428                return Some(("group_relay_broadcast", payload.to_string()));
5429            }
5430        }
5431        None
5432    }
5433
5434    pub(super) fn handle_outbound_media_chunk_delivered(&mut self, message_id: &MessageId) {
5435        let Some((file_id, chunk_index)) = self.outbound_media_chunks.remove(message_id) else {
5436            return;
5437        };
5438
5439        if let Some(window) = self.outbound_media_windows.get_mut(&file_id) {
5440            window.on_chunk_ack(chunk_index);
5441        }
5442
5443        let Some(transfer) = self.outbound_media_transfers.get_mut(&file_id) else {
5444            return;
5445        };
5446
5447        transfer.delivered_chunks.insert(chunk_index);
5448        transfer.last_updated_at = Instant::now();
5449
5450        let delivered_chunks = transfer.delivered_chunks.len() as u32;
5451        let total_chunks = transfer.total_chunks;
5452        let content_type = transfer.content_type;
5453        let recipient = transfer.recipient.clone();
5454        let completed = delivered_chunks == total_chunks;
5455        let carries_data = transfer.data_purpose.is_some();
5456
5457        if !carries_data {
5458            if let Ok(state) = lock_shared_state(&self.shared_state) {
5459                state.emit_event(Event::file_progress(
5460                    file_id.clone(),
5461                    delivered_chunks,
5462                    total_chunks,
5463                ));
5464                if completed {
5465                    state.emit_event(Event::media_sent(file_id.clone(), content_type, recipient));
5466                }
5467            }
5468        }
5469
5470        if completed {
5471            self.outbound_media_transfers.remove(&file_id);
5472            self.outbound_media_windows.remove(&file_id);
5473            self.remove_media_descriptor(&file_id);
5474        }
5475    }
5476
5477    pub(super) fn handle_outbound_media_chunk_failed(
5478        &mut self,
5479        message_id: &MessageId,
5480        reason: &str,
5481    ) {
5482        let Some((file_id, _chunk_index)) = self.outbound_media_chunks.remove(message_id) else {
5483            return;
5484        };
5485        self.abort_outbound_media_transfer(&file_id, reason);
5486    }
5487
5488    /// Aborts an active outbound media transfer: removes all transfer
5489    /// tracking (freeing its per-peer transfer slot) and emits
5490    /// [`Event::MediaSendFailed`] so the app learns the transfer will never
5491    /// complete. Idempotent — a second call for the same `file_id` is a no-op.
5492    ///
5493    /// A data-purposed transfer is the one exception to the event: it has no
5494    /// application-facing identity, so the report is withheld rather than
5495    /// naming a `file_id` the app never saw.
5496    pub(super) fn abort_outbound_media_transfer(&mut self, file_id: &str, reason: &str) {
5497        let Some(transfer) = self.outbound_media_transfers.remove(file_id) else {
5498            return;
5499        };
5500        self.outbound_media_chunks
5501            .retain(|_, (candidate_file_id, _)| candidate_file_id.as_str() != file_id);
5502        self.outbound_media_windows.remove(file_id);
5503        // A terminal abort is settled state — the app hears MediaSendFailed
5504        // now, so a MediaResendRequired for it after a restart would be
5505        // stale noise.
5506        self.remove_media_descriptor(file_id);
5507        warn!(
5508            file_id = %file_id,
5509            reason = %reason,
5510            "Aborting outbound media transfer"
5511        );
5512        // A data-purposed transfer has no application-facing identity to
5513        // fail, so reporting one would name a file_id the app has never
5514        // seen. The requester's own fetch expires and can be asked again,
5515        // and a snapshot is re-offered by the next anti-entropy exchange, so
5516        // nothing is stranded by staying quiet here.
5517        if transfer.data_purpose.is_some() {
5518            debug!(
5519                file_id = %file_id,
5520                reason = %reason,
5521                "A data-layer transfer was aborted; the asking side will retry"
5522            );
5523            return;
5524        }
5525        if let Ok(state) = lock_shared_state(&self.shared_state) {
5526            state.emit_event(Event::media_send_failed(
5527                file_id.to_string(),
5528                transfer.recipient,
5529                reason.to_string(),
5530            ));
5531        }
5532    }
5533
5534    pub(super) fn cleanup_stale_media_state(&mut self, max_age: StdDuration) {
5535        let now = Instant::now();
5536
5537        self.pending_media_metadata
5538            .retain(|_, metadata| now.duration_since(metadata.last_updated_at) <= max_age);
5539
5540        let stale_outbound_file_ids: HashSet<String> = self
5541            .outbound_media_transfers
5542            .iter()
5543            .filter_map(|(file_id, transfer)| {
5544                if now.duration_since(transfer.last_updated_at) > max_age {
5545                    return Some(file_id.clone());
5546                }
5547                None
5548            })
5549            .collect();
5550
5551        if stale_outbound_file_ids.is_empty() {
5552            return;
5553        }
5554
5555        self.outbound_media_transfers
5556            .retain(|file_id, _| !stale_outbound_file_ids.contains(file_id));
5557        self.outbound_media_chunks
5558            .retain(|_, (file_id, _)| !stale_outbound_file_ids.contains(file_id));
5559        self.outbound_media_windows
5560            .retain(|file_id, _| !stale_outbound_file_ids.contains(file_id));
5561        for file_id in &stale_outbound_file_ids {
5562            self.remove_media_descriptor(file_id);
5563        }
5564    }
5565
5566    // ========================================================================
5567    // ACK
5568    // ========================================================================
5569
5570    pub(super) fn send_delivery_ack(
5571        &mut self,
5572        message: &Message,
5573        inbound_transport: TransportType,
5574    ) -> Result<()> {
5575        self.send_ack_with_status(message, inbound_transport, None)
5576    }
5577
5578    /// Answers a frame that can never decrypt: a delivery ACK in shape (so a
5579    /// relay holding the frame drops it) carrying
5580    /// `ack_status = "undecryptable"`, which the sender settles without
5581    /// `message_delivered` (see [`Self::handle_undecryptable_ack`]). Only for
5582    /// a frame that asked for an answer; the plain ACK's rules otherwise apply.
5583    ///
5584    /// The one gate for every path that gives up on a frame (receive loop,
5585    /// queue drain, queue eviction): only a sender that advertised
5586    /// [`ACK_UNDECRYPTABLE_V1`](offline_protocol_sealed::ACK_UNDECRYPTABLE_V1)
5587    /// is answered. An older SDK reads any ACK as a delivery, so it gets what
5588    /// it always got (nothing) and keeps its retries.
5589    ///
5590    /// And only a frame that arrived over the internet relay. Every frame this
5591    /// answers failed before anything authenticated it: garbage ciphertext in
5592    /// a derivable session slot, or a parked frame whose sender is only a
5593    /// claim. Answering on a mesh or peer-stream link would tell an injector
5594    /// in radio range that the target is live and processing, which the
5595    /// threat model's acknowledgement rule withholds. The relay already knows
5596    /// the device is connected, and its mailbox is the one thing that
5597    /// redelivers a dead frame forever, so it is the one carrier answered.
5598    pub(super) fn send_undecryptable_ack(
5599        &mut self,
5600        message: &Message,
5601        inbound_transport: TransportType,
5602    ) {
5603        if inbound_transport != TransportType::Internet
5604            || !message.requires_ack
5605            || !self
5606                .peer_undecryptable_ack
5607                .contains(message.sender.as_str())
5608        {
5609            return;
5610        }
5611        let ack = match self.build_ack(message, inbound_transport, Some(ACK_STATUS_UNDECRYPTABLE)) {
5612            Ok(ack) => ack,
5613            Err(err) => {
5614                error!(
5615                    message_id = %message.id,
5616                    error = %err,
5617                    "Failed to build undecryptable ACK"
5618                );
5619                return;
5620            }
5621        };
5622        // Pinned to the relay, never the ACK ladder: `route_ack` falls back to
5623        // the mesh and to whatever DORS picks when the relay is down, and a
5624        // held ACK re-runs that same ladder later. Either would put this
5625        // answer on a radio link, where R25 says it never goes. Dropped
5626        // instead: the relay mailbox redelivers the frame on the next
5627        // connect, and the live path answers it then.
5628        let sent = self
5629            .transport_manager
5630            .can_address_via(TransportType::Internet, message.sender.as_str())
5631            && self
5632                .transport_manager
5633                .send_via_transport(&ack, TransportType::Internet)
5634                .is_ok();
5635        if !sent {
5636            debug!(
5637                message_id = %message.id,
5638                "Relay unavailable; undecryptable ACK dropped until the frame is redelivered"
5639            );
5640        }
5641    }
5642
5643    fn send_ack_with_status(
5644        &mut self,
5645        message: &Message,
5646        inbound_transport: TransportType,
5647        status: Option<&str>,
5648    ) -> Result<()> {
5649        if status.is_none() && self.try_coalesce_ack(message, inbound_transport) {
5650            return Ok(());
5651        }
5652        let ack = self.build_ack(message, inbound_transport, status)?;
5653        self.route_ack(&ack, inbound_transport)
5654    }
5655
5656    /// Holds the delivery ACK for `message` to go out with others to the same
5657    /// peer, when that peer reads coalesced ACKs and the frame came over a
5658    /// mesh link. Returns `false` when the ACK must go on its own.
5659    ///
5660    /// Only the mesh carriers: they are where a phone's ACKs were a third of
5661    /// the frames it sent, on a link carrying less than it was offered (the
5662    /// 2026-10-19 volume test). A relay frame costs a round trip to a server,
5663    /// not a share of a radio link. An `undecryptable` answer is never held:
5664    /// it is not a delivery, and only a delivery may share a frame.
5665    fn try_coalesce_ack(&mut self, message: &Message, inbound_transport: TransportType) -> bool {
5666        if !matches!(
5667            inbound_transport,
5668            TransportType::BLE | TransportType::WiFiDirect
5669        ) || !self.peer_coalesced_ack.contains(message.sender.as_str())
5670        {
5671            return false;
5672        }
5673        let peer = message.sender.as_str().to_string();
5674        if self
5675            .coalesced_acks_out
5676            .get(&peer)
5677            .is_some_and(|batch| batch.transport != inbound_transport)
5678        {
5679            self.flush_coalesced_acks_for(&peer);
5680        }
5681        let batch = self
5682            .coalesced_acks_out
5683            .entry(peer.clone())
5684            .or_insert_with(|| CoalescedAcks {
5685                transport: inbound_transport,
5686                since: Instant::now(),
5687                hop_count: message.hop_count.value().to_string(),
5688                ids: Vec::new(),
5689            });
5690        let id = message.id.as_str();
5691        if !batch.ids.contains(&id) {
5692            batch.ids.push(id);
5693        }
5694        if batch.ids.len() >= ACK_COALESCE_MAX {
5695            self.flush_coalesced_acks_for(&peer);
5696        }
5697        true
5698    }
5699
5700    /// Sends every held ACK batch older than [`ACK_COALESCE_WINDOW`].
5701    /// Called each tick.
5702    pub(super) fn flush_due_coalesced_acks(&mut self) {
5703        if self.coalesced_acks_out.is_empty() {
5704            return;
5705        }
5706        let due: Vec<String> = self
5707            .coalesced_acks_out
5708            .iter()
5709            .filter(|(_, batch)| batch.since.elapsed() >= ACK_COALESCE_WINDOW)
5710            .map(|(peer, _)| peer.clone())
5711            .collect();
5712        for peer in due {
5713            self.flush_coalesced_acks_for(&peer);
5714        }
5715    }
5716
5717    /// Sends the ACK batch held for `peer` as one frame: the first id in
5718    /// [`ACK_FOR_KEY`], so even a peer that ignores [`ACK_MORE_KEY`] settles
5719    /// one, and the rest in [`ACK_MORE_KEY`].
5720    fn flush_coalesced_acks_for(&mut self, peer: &str) {
5721        let Some(batch) = self.coalesced_acks_out.remove(peer) else {
5722            return;
5723        };
5724        let Some((first, more)) = batch.ids.split_first() else {
5725            return;
5726        };
5727        let built = (|| -> Result<Message> {
5728            let ttl = TTL::new(self.config.initial_ttl).unwrap_or_else(|_| TTL::default());
5729            let mut builder = Message::builder(
5730                UserId::new(&self.local_id)?,
5731                UserId::new(peer)?,
5732                AppId::new(&self.config.app_id)?,
5733            )
5734            .content(String::new())
5735            .priority(MessagePriority::Low)
5736            .ttl(ttl)
5737            .requires_ack(false)
5738            .metadata(ACK_FOR_KEY, first.as_str())
5739            .metadata(ACK_HOP_COUNT_KEY, batch.hop_count.as_str())
5740            .metadata(ACK_TRANSPORT_KEY, Self::transport_label(batch.transport));
5741            if !more.is_empty() {
5742                builder = builder.metadata(ACK_MORE_KEY, more.join(","));
5743            }
5744            Ok(builder.build())
5745        })();
5746        let ack = match built {
5747            Ok(ack) => ack,
5748            Err(err) => {
5749                warn!(peer = %peer, error = %err, "Failed to build a coalesced delivery ACK");
5750                return;
5751            }
5752        };
5753        if let Err(err) = self.route_ack(&ack, batch.transport) {
5754            debug!(peer = %peer, error = %err, "Coalesced delivery ACK not routed");
5755        }
5756    }
5757
5758    /// Builds the acknowledgement for `message`, optionally carrying
5759    /// [`ACK_STATUS_KEY`]. Routing is the caller's.
5760    fn build_ack(
5761        &self,
5762        message: &Message,
5763        inbound_transport: TransportType,
5764        status: Option<&str>,
5765    ) -> Result<Message> {
5766        let sender = UserId::new(&self.local_id)?;
5767        let recipient = message.sender.clone();
5768        let app_id = AppId::new(&self.config.app_id)?;
5769        let ttl = TTL::new(self.config.initial_ttl).unwrap_or_else(|_| TTL::default());
5770
5771        let mut builder = Message::builder(sender, recipient, app_id)
5772            .content(String::new())
5773            .priority(MessagePriority::Low)
5774            .ttl(ttl)
5775            .requires_ack(false)
5776            .metadata(ACK_FOR_KEY, message.id.as_str())
5777            .metadata(ACK_HOP_COUNT_KEY, message.hop_count.value().to_string())
5778            .metadata(ACK_TRANSPORT_KEY, Self::transport_label(inbound_transport));
5779        if let Some(status) = status {
5780            builder = builder.metadata(ACK_STATUS_KEY, status);
5781        }
5782
5783        Ok(builder.build())
5784    }
5785
5786    /// Gets an acknowledgement back to whoever sent us the message it answers.
5787    ///
5788    /// The single route-selection point for every delivery ACK, direct or
5789    /// group. Keeping one ladder is deliberate: it has four callers across two
5790    /// builders, and the failure this ordering exists to prevent is silent —
5791    /// a copy of the ladder that drifted would look fine and lose answers.
5792    ///
5793    /// Three routes, in order:
5794    ///
5795    /// 1. **The link it arrived on**, when that carrier can actually address
5796    ///    the sender. Preferred because it is known to work for this peer.
5797    /// 2. **The mesh**, when the sender is not a direct mesh neighbor and
5798    ///    either nothing we hold reaches them directly *or* the message itself
5799    ///    arrived over a mesh carrier: the answer travels back the way the
5800    ///    message came, carried by the devices in between. Without this,
5801    ///    multi-hop delivery looks like failure to a sender who retransmits a
5802    ///    message we already have and read.
5803    /// 3. **DORS**, which picks among whatever else is available.
5804    ///
5805    /// The arrival-over-mesh half of step 2 is what makes this work in a mixed
5806    /// neighborhood. A frame that reached us across the mesh came from someone
5807    /// the mesh can reach and — having failed step 1 — someone we cannot hand it
5808    /// straight back to, which says nothing about whether *our* internet is up.
5809    /// Deciding from local carrier status alone, an online recipient answers a
5810    /// mesh-delivered message over the relay, where an offline sender cannot see
5811    /// it: delivered and read, reported failed. In that case the answer goes to
5812    /// the mesh **and** on to step 3, since an online sender is equally
5813    /// plausible and a duplicate acknowledgement costs one frame the sender's
5814    /// ack handling already absorbs. When nothing reaches the sender directly
5815    /// the mesh remains the whole answer and step 3 is skipped, as before.
5816    ///
5817    /// Step 1 is *gated on addressability* as well as on the send result. A
5818    /// transport returning `Ok` is not evidence the frame can arrive: a mesh
5819    /// carrier that enqueued for any recipient (Wi-Fi Direct did, before it
5820    /// refused a recipient no stream had proved) would, on the last hop of a
5821    /// forwarded frame, report the answer as sent toward a sender it holds no
5822    /// link to, and step 2 would never run. The sender's retransmissions would
5823    /// take the same path every time, ending in a failure report for a
5824    /// delivered message.
5825    ///
5826    /// An acknowledgement no route takes is held, not dropped
5827    /// ([`Self::hold_unrouted_ack`]): it was owed the moment the message was
5828    /// accepted, and the common miss is a carrier that is merely not up yet —
5829    /// a push-injected frame acknowledged before the relay socket has
5830    /// authenticated. Held is reported as `Ok`, like any carrier queue.
5831    fn route_ack(&mut self, ack_message: &Message, inbound_transport: TransportType) -> Result<()> {
5832        if self.try_route_ack(ack_message, inbound_transport) {
5833            return Ok(());
5834        }
5835        debug!(
5836            ack_to = %ack_message.recipient,
5837            "No route back for a delivery acknowledgement; holding it for the next carrier edge"
5838        );
5839        self.hold_unrouted_ack(ack_message.clone(), inbound_transport, Instant::now());
5840        Ok(())
5841    }
5842
5843    /// Holds an acknowledgement until [`Self::flush_unrouted_acks`]. Bounded
5844    /// both ways: [`MAX_UNROUTED_ACKS`] (oldest dropped first) and
5845    /// [`UNROUTED_ACK_TTL`]. Nothing past either is lost for good — a sender
5846    /// that never hears back re-sends, and the duplicate is re-ACKed.
5847    fn hold_unrouted_ack(&mut self, ack: Message, inbound: TransportType, held_at: Instant) {
5848        // One ACK answers every copy of a message, and a peer replaying it
5849        // while no carrier is up would otherwise spend a slot per copy.
5850        let acked = ack.metadata.get(ACK_FOR_KEY).cloned();
5851        self.unrouted_acks.retain(|(held, _, _)| {
5852            held.recipient != ack.recipient || held.metadata.get(ACK_FOR_KEY).cloned() != acked
5853        });
5854        if self.unrouted_acks.len() >= MAX_UNROUTED_ACKS {
5855            self.unrouted_acks.pop_front();
5856            warn!("Unrouted acknowledgement queue full; dropped the oldest");
5857        }
5858        self.unrouted_acks.push_back((ack, inbound, held_at));
5859    }
5860
5861    /// Re-routes every held acknowledgement through the same ladder. Run on
5862    /// the carrier-level edge (`flush_outbox_all`: start, and every carrier's
5863    /// false→true), ahead of the outbox, because an ACK is what lets the
5864    /// peer's outbox stop re-sending to us. Still unroutable stays held, on
5865    /// its original clock.
5866    ///
5867    /// A peer blocked since its ACK was held gets nothing: the receive path
5868    /// never answers a blocked sender, so its presence is not leaked, and a
5869    /// held ACK must not reverse that decision late.
5870    pub(super) fn flush_unrouted_acks(&mut self) {
5871        for (ack, inbound, held_at) in std::mem::take(&mut self.unrouted_acks) {
5872            if held_at.elapsed() > UNROUTED_ACK_TTL
5873                || self.is_user_blocked(ack.recipient.as_str())
5874                || self.try_route_ack(&ack, inbound)
5875            {
5876                continue;
5877            }
5878            self.hold_unrouted_ack(ack, inbound, held_at);
5879        }
5880    }
5881
5882    /// The ladder itself; `true` when some route took the acknowledgement.
5883    fn try_route_ack(&mut self, ack_message: &Message, inbound_transport: TransportType) -> bool {
5884        let ack_to = ack_message.recipient.as_str().to_string();
5885
5886        if self
5887            .transport_manager
5888            .can_address_via(inbound_transport, &ack_to)
5889            && self
5890                .transport_manager
5891                .send_via_transport(ack_message, inbound_transport)
5892                .is_ok()
5893        {
5894            return true;
5895        }
5896
5897        debug!(
5898            ack_to = %ack_to,
5899            inbound_transport = ?inbound_transport,
5900            "Inbound transport cannot answer this sender; looking for another route"
5901        );
5902
5903        // No route at all without help: the mesh is the entire answer.
5904        let no_direct_route = !self.can_reach_recipient(&ack_to);
5905        // Reached us through other devices, and we cannot hand it straight
5906        // back: the way it came is a route we know exists, whatever our own
5907        // carriers claim.
5908        let carried_to_us = TransportManager::is_mesh_transport(inbound_transport)
5909            && !self.transport_manager.mesh_can_address(&ack_to);
5910
5911        let mut handed_to_mesh = 0usize;
5912        if no_direct_route || carried_to_us {
5913            handed_to_mesh = self.offer_to_mesh(ack_message);
5914            if handed_to_mesh > 0 {
5915                debug!(
5916                    ack_to = %ack_to,
5917                    handed_to_mesh,
5918                    "Handed the acknowledgement to the mesh"
5919                );
5920                if no_direct_route {
5921                    return true;
5922                }
5923            }
5924        }
5925
5926        // Neighbors took it, so the answer is on its way even though nothing
5927        // else could carry it.
5928        self.transport_manager.send(ack_message).is_ok() || handed_to_mesh > 0
5929    }
5930
5931    /// Builds and sends a delivery ACK from the group-message drain, which
5932    /// holds only the acked id and the original wire sender (not the full
5933    /// inbound [`Message`]). Mirrors [`Self::send_delivery_ack`]; the hop count
5934    /// is unknown at drain time and reported as 0 (sender-side metrics only).
5935    pub(crate) fn send_group_delivery_ack(
5936        &mut self,
5937        ack_to: &str,
5938        acked_message_id: &str,
5939        inbound_transport: TransportType,
5940    ) -> Result<()> {
5941        let sender = UserId::new(&self.local_id)?;
5942        let recipient = UserId::new(ack_to)?;
5943        let app_id = AppId::new(&self.config.app_id)?;
5944        let ttl = TTL::new(self.config.initial_ttl).unwrap_or_else(|_| TTL::default());
5945
5946        let ack_message = Message::builder(sender, recipient, app_id)
5947            .content(String::new())
5948            .priority(MessagePriority::Low)
5949            .ttl(ttl)
5950            .requires_ack(false)
5951            .metadata(ACK_FOR_KEY, acked_message_id)
5952            .metadata(ACK_HOP_COUNT_KEY, "0")
5953            .metadata(ACK_TRANSPORT_KEY, Self::transport_label(inbound_transport))
5954            .build();
5955
5956        // Same ladder as a direct-message ACK, mesh fallback included: a group
5957        // frame reaches its members over the same hops a DM does.
5958        self.route_ack(&ack_message, inbound_transport)
5959    }
5960
5961    pub(super) fn handle_ack_message(&mut self, message: &Message) {
5962        // A coalesced ACK is settled as the single ACKs it stands for, so
5963        // each id passes every check below on its own, the sender-equality
5964        // check first. Never on an `undecryptable` answer, which is not a
5965        // delivery and is never coalesced; and no more ids than a sender
5966        // coalesces, so a forged one reaches no further than five frames
5967        // could.
5968        if message.metadata.contains_key(ACK_MORE_KEY) {
5969            let mut single = message.clone();
5970            let more = single.metadata.remove(ACK_MORE_KEY).unwrap_or_default();
5971            self.handle_ack_message(&single);
5972            if !single.metadata.contains_key(ACK_STATUS_KEY) {
5973                for id in more
5974                    .split(',')
5975                    .filter(|id| !id.is_empty())
5976                    .take(ACK_COALESCE_MAX - 1)
5977                {
5978                    single
5979                        .metadata
5980                        .insert(ACK_FOR_KEY.to_string(), id.to_string());
5981                    self.handle_ack_message(&single);
5982                }
5983            }
5984            return;
5985        }
5986        if let Some(ack_for) = message.metadata.get(ACK_FOR_KEY) {
5987            if let Ok(message_id) = MessageId::from_str(ack_for) {
5988                // Whoever answers has to be whoever we sent it to. An
5989                // acknowledgement names a message id and nothing else ties it
5990                // to a delivery, so without this any third party that merely
5991                // saw the frame — a mesh carrier that took a copy, the relay —
5992                // settles it: the outbox entry is dropped, the retries stop and
5993                // the app is told `message_delivered` for a message that
5994                // arrived nowhere. Refusing delivery is something those parties
5995                // could always do; a false confirmation is the strictly worse
5996                // one, because it is the version the sender never finds out
5997                // about.
5998                //
5999                // Checked before anything is removed. The settle work below
6000                // starts by taking the pending ACK, and a frame rejected after
6001                // that point would have already cost the message its retry
6002                // record — turning a refused acknowledgement into the silent
6003                // loss this exists to prevent.
6004                //
6005                // Safe on this path — which every delivery passes through —
6006                // because delivery already implies the equality: a device
6007                // processes a frame only when its own `local_id` equals the
6008                // frame's recipient (`receive_message` forwards anything else),
6009                // and the acknowledgement it builds carries that same
6010                // `local_id` as its sender. A genuine confirmation therefore
6011                // matches whatever identifier namespace the app addresses peers
6012                // in — the profile before MLS initialization, the `off1…`
6013                // address after it.
6014                //
6015                // It is **not** authentication and must not be read as one: an
6016                // acknowledgement carries no signature (empty content is not a
6017                // security-gated prefix, and the receive loop handles it before
6018                // the gate would run anyway), so a forger who saw the frame saw
6019                // its recipient too and can simply write that name. What this
6020                // removes is the unattributed forgery, and the divergence
6021                // between the two settle paths below. Binding an
6022                // acknowledgement to its sender for real needs signed acks.
6023                if !self.ack_is_from_the_message_recipient(&message_id, message) {
6024                    debug!(
6025                        message_id = %message_id,
6026                        claimed_sender = %message.sender,
6027                        "Ignoring an acknowledgement from someone this message was not sent to"
6028                    );
6029                    return;
6030                }
6031
6032                // Answered, not delivered: never `message_delivered`.
6033                if message.metadata.get(ACK_STATUS_KEY).map(String::as_str)
6034                    == Some(ACK_STATUS_UNDECRYPTABLE)
6035                {
6036                    self.handle_undecryptable_ack(&message_id);
6037                    return;
6038                }
6039
6040                // The peer decrypted this frame, so it has reached at least
6041                // its place in the session, which re-opens the re-seal budget
6042                // up to there. Unauthenticated like the rest of this path: a
6043                // forged acknowledgement can only give back re-seals, which is
6044                // where every sender was before the budget existed.
6045                if let Some(mark) = self
6046                    .outbox
6047                    .get(&message_id)
6048                    .and_then(|entry| entry.reseal.as_ref())
6049                    .and_then(|provenance| provenance.seal_mark)
6050                {
6051                    if let Some(mls) = self.mls_manager.as_ref().and_then(|m| m.read().ok()) {
6052                        mls.note_acknowledged_seal(message.sender.as_str(), mark);
6053                    }
6054                }
6055
6056                // A delivery ack settles an outbound connection request:
6057                // the recipient provably received it, so a later stale
6058                // recipient_unreachable signal must not fire a false
6059                // ConnectionRequestUndeliverable. Outside the ack_manager
6060                // branch — a duplicate ack (second transport) or one
6061                // arriving after retry exhaustion is still proof.
6062                self.pending_connection_requests
6063                    .remove(&message_id.as_str());
6064                if let Some(pending) = self.ack_manager.remove_ack(&message_id) {
6065                    let latency = Utc::now()
6066                        .signed_duration_since(pending.sent_at)
6067                        .num_milliseconds()
6068                        .max(0) as u64;
6069
6070                    let hop_count = message
6071                        .metadata
6072                        .get(ACK_HOP_COUNT_KEY)
6073                        .and_then(|v| v.parse::<u8>().ok())
6074                        .unwrap_or(0);
6075
6076                    let transport = message
6077                        .metadata
6078                        .get(ACK_TRANSPORT_KEY)
6079                        .map(|label| Self::transport_from_label(label))
6080                        .unwrap_or(TransportType::BLE);
6081
6082                    // `latency` above is this attempt's round trip, which is
6083                    // what DORS scores a carrier on. The event reports how long
6084                    // delivery took, from the first send, as the settle path
6085                    // without a pending ACK already does: a message that was
6086                    // deferred, or re-sent after its ACK timed out, otherwise
6087                    // reported only its last attempt, and the two paths gave
6088                    // the same delivery two different latencies.
6089                    let outbox_entry = self.outbox.get(&message_id);
6090                    let delivery_latency = outbox_entry.map_or(latency, |entry| {
6091                        Utc::now()
6092                            .signed_duration_since(entry.first_sent_at)
6093                            .num_milliseconds()
6094                            .max(0) as u64
6095                    });
6096                    let app_send = outbox_entry.is_some_and(|entry| entry.app_send);
6097
6098                    // Emit event if we can lock the state, but don't fail if we can't
6099                    if let Ok(state) = lock_shared_state(&self.shared_state) {
6100                        let event = Event::message_delivered(
6101                            message_id.clone(),
6102                            delivery_latency,
6103                            hop_count,
6104                            transport,
6105                        );
6106                        state.emit_settlement(event, app_send);
6107                        drop(state);
6108                    } else {
6109                        error!(
6110                            "Failed to lock shared state for ACK event, skipping event emission"
6111                        );
6112                    }
6113
6114                    self.transport_manager.reset_retry_count(transport);
6115                    self.transport_manager.record_delivery_success(
6116                        transport,
6117                        latency.min(u32::MAX as u64) as u32,
6118                        hop_count,
6119                    );
6120                    self.handle_outbound_media_chunk_delivered(&message_id);
6121                    self.retry_queue.remove(&message_id.as_str());
6122                    if let Some(entry) = self.remove_outbox_entry(&message_id) {
6123                        // Delivery proves reachability, so the escalating
6124                        // unreachable-probe interval starts over. Clearing the
6125                        // counter is not enough on its own: it is per-peer
6126                        // while the probes are per-message, so a burst of DMs
6127                        // to an offline peer escalates the shared ladder once
6128                        // per park and leaves the later ones scheduled minutes
6129                        // out (at the 600s cap after seven). Delivery of any
6130                        // one of them proves the rest can go now, and on a
6131                        // consumer that never polls presence this ACK is the
6132                        // only edge that will say so. Flushing owns the
6133                        // counter reset — including restoring it when every
6134                        // re-drive fails — so this must not pre-clear it.
6135                        //
6136                        // The override is our own record of the transport the
6137                        // delivered send went out on (`last_transport`), which
6138                        // this ACK just proved reaches the peer — genuine
6139                        // per-transport reachability, pinning the re-drive the
6140                        // way presence-online pins Internet. Deliberately NOT
6141                        // the ACK's `transport` above: ACK_TRANSPORT_KEY is
6142                        // peer-supplied, and `from_label` maps any absent,
6143                        // stale, or hostile label to BLE — pinning a burst of
6144                        // sibling sends to a carrier chosen by the peer.
6145                        // Filtered on local availability (the transport may
6146                        // have dropped since the send); `None` falls back to
6147                        // DORS, so a missing carrier costs routing freedom,
6148                        // never the re-drive itself.
6149                        let recipient = entry.message.recipient.as_str().to_string();
6150                        if self.dm_unreachable_parks.contains_key(&recipient) {
6151                            let redrive_via = entry.last_transport.filter(|t| {
6152                                self.transport_manager
6153                                    .get_available_transports()
6154                                    .contains_key(t)
6155                            });
6156                            self.flush_outbox_for_peer_via(&recipient, redrive_via);
6157                        }
6158                    }
6159                } else {
6160                    // No pending ACK. Ordinarily that means a duplicate answer
6161                    // or one arriving after the message already settled, but a
6162                    // parked DM has none either, nor does one the mesh carried
6163                    // after no carrier took it directly, and their delivery is
6164                    // the one this device has no other way to learn about.
6165                    self.settle_dm_without_pending_ack(&message_id, message);
6166                }
6167            }
6168        }
6169    }
6170
6171    /// Settles an `undecryptable` answer: the recipient cannot decrypt the
6172    /// copy it holds. An encrypted DM is resent once, same id: re-sealed under
6173    /// the current session when its plaintext is still in memory (the flush
6174    /// path re-seals), verbatim when it is not (an entry restored after a
6175    /// restart). Verbatim is not futile: a frame the recipient's
6176    /// pending-decryption queue gave up on (TTL or overflow) was never
6177    /// attempted, so the same ciphertext decrypts once the session is up.
6178    /// Failing it here instead would turn a recoverable drop into a terminal
6179    /// one, and since a parked frame's sender is unauthenticated, anyone
6180    /// able to fill the queue could trigger that. A second answer for an id
6181    /// already resent leaves the entry to its retry ladder, which re-seals on
6182    /// every attempt it can and ends in `message_failed` at max retries: that
6183    /// bounds two devices trading answers and resends at network speed, and
6184    /// keeps a split-brain pair's message deliverable once the session is
6185    /// rebuilt. Anything else (no entry left, plaintext, media) is ignored,
6186    /// which is exactly what the frame's failure used to cost it.
6187    fn handle_undecryptable_ack(&mut self, message_id: &MessageId) {
6188        let Some(entry) = self.outbox.get(message_id) else {
6189            return;
6190        };
6191        if !entry
6192            .message
6193            .content
6194            .starts_with(internal_prefixes::ENCRYPTED)
6195        {
6196            return;
6197        }
6198        if !self.undecryptable_resent.insert(message_id.clone()) {
6199            return;
6200        }
6201        let (message, attempt_count) = (entry.message.clone(), entry.attempt_count);
6202        debug!(
6203            message_id = %message_id,
6204            "Recipient cannot decrypt this copy; resending once"
6205        );
6206        self.retry_queue.remove(&message_id.as_str());
6207        self.ack_manager.remove_ack(message_id);
6208        self.try_flush_send(message, attempt_count);
6209    }
6210
6211    /// Whether `ack` comes from the peer the message it answers was addressed
6212    /// to — the one thing about an acknowledgement that is not simply asserted
6213    /// by whoever sent it. See [`Self::handle_ack_message`] for what that does
6214    /// and does not buy.
6215    ///
6216    /// Both outboxes are consulted because both hold ack-bearing sends: plain
6217    /// DMs and control frames in `outbox`, media chunks in `media_outbox`.
6218    ///
6219    /// Answers `true` when neither holds the id, which is the ordinary shape of
6220    /// a duplicate answer arriving after the message already settled — there is
6221    /// no record left to judge against, and inventing a verdict would start
6222    /// dropping the answers this device has always accepted. The fail-open is
6223    /// not reachable by an attacker: it needs our own outbox entry to be gone,
6224    /// which nothing a peer sends can arrange.
6225    fn ack_is_from_the_message_recipient(&self, message_id: &MessageId, ack: &Message) -> bool {
6226        let Some(entry) = self
6227            .outbox
6228            .get(message_id)
6229            .or_else(|| self.media_outbox.get(message_id))
6230        else {
6231            return true;
6232        };
6233        ack.sender.as_str() == entry.message.recipient.as_str()
6234    }
6235
6236    /// Settles a DM whose acknowledgement arrived with no pending ACK to match
6237    /// it: the delivery the sender can otherwise never learn about.
6238    ///
6239    /// Invariant: a plain DM still in the outbox is settled by its recipient's
6240    /// acknowledgement, whichever copy of it arrived. A pending ACK exists only
6241    /// for a send some carrier accepted, and two paths hand a DM to the mesh
6242    /// without one: a park, which removes it so the retry budget stops burning
6243    /// against a peer the relay says is offline, and a direct send that no
6244    /// carrier took, which never registered one. On both, a neighbour can
6245    /// carry the frame to the recipient and the answer lands here. Before this
6246    /// arm took the second path, a message to a peer two hops away was
6247    /// delivered and read, and its sender kept it in the outbox, retrying,
6248    /// until the outbox lifetime failed it seven days later.
6249    ///
6250    /// The offer and this arm are one change; neither is correct alone.
6251    ///
6252    /// Gated twice, because an acknowledgement is unauthenticated (it carries
6253    /// no internal prefix, so it never reaches the control gate, and it is
6254    /// handled before the security gate runs on the receive path):
6255    ///
6256    /// 1. the id is a plain DM ([`Self::is_parkable_plain_dm`]): welcomes keep
6257    ///    their own lifecycle, and media chunks keep their pending ACK and
6258    ///    settle on the branch above. This gate does **not** exclude connection
6259    ///    requests: the caller retires their typed tracking a few lines earlier
6260    ///    and unconditionally, so by the time it runs the map no longer holds
6261    ///    the id. A request carried to its recipient and answered was
6262    ///    delivered, and settling it is the same proof the pending-ACK branch
6263    ///    acts on;
6264    /// 2. it is still in the outbox, so a late or duplicate answer to a
6265    ///    message that already settled finds nothing to act on.
6266    ///
6267    /// Attribution, that the answer comes from the recipient rather than from
6268    /// any party that handled the frame, is applied by
6269    /// [`Self::handle_ack_message`] to every acknowledgement it accepts, so
6270    /// both settle paths judge by one rule against one record.
6271    ///
6272    /// What remains is a forgery by someone who saw the frame: a carrier or the
6273    /// relay, parties who can already deny delivery outright and who can forge
6274    /// an acknowledgement for an in-flight message on the branch above. An
6275    /// earlier third gate, a live park counter, narrowed that to parties who
6276    /// also knew the relay refused the frame; it is gone because it is what
6277    /// dropped every answer to a message the mesh carried without a park.
6278    ///
6279    /// Reports latency end to end (from the first send) rather than per attempt:
6280    /// there is no pending record to measure the last attempt against, and for a
6281    /// message that waited the honest number is how long delivery actually
6282    /// took. For the same reason no transport metric is recorded: the carrier
6283    /// label is peer-supplied and there is no attempt to attribute it to.
6284    fn settle_dm_without_pending_ack(&mut self, message_id: &MessageId, ack: &Message) {
6285        if !self.is_parkable_plain_dm(message_id) {
6286            return;
6287        }
6288        let Some(entry) = self.outbox.get(message_id) else {
6289            return;
6290        };
6291        let recipient = entry.message.recipient.as_str().to_string();
6292        let app_send = entry.app_send;
6293
6294        let latency = Utc::now()
6295            .signed_duration_since(entry.first_sent_at)
6296            .num_milliseconds()
6297            .max(0) as u64;
6298        let hop_count = ack
6299            .metadata
6300            .get(ACK_HOP_COUNT_KEY)
6301            .and_then(|v| v.parse::<u8>().ok())
6302            .unwrap_or(0);
6303        let transport = ack
6304            .metadata
6305            .get(ACK_TRANSPORT_KEY)
6306            .map(|label| Self::transport_from_label(label))
6307            .unwrap_or(TransportType::BLE);
6308
6309        debug!(
6310            message_id = %message_id,
6311            recipient = %recipient,
6312            latency_ms = latency,
6313            "DM with no pending acknowledgement was delivered; settling from its acknowledgement"
6314        );
6315
6316        self.retry_queue.remove(&message_id.as_str());
6317        let redrive_via = self.remove_outbox_entry(message_id).and_then(|entry| {
6318            // Same reasoning as the pending-ACK path: our own record of where
6319            // the delivered send went, never the peer-supplied label, and only
6320            // while that carrier is still up. The park counter stays for the
6321            // flush to reset, so a re-drive that fails everywhere restores it.
6322            entry.last_transport.filter(|t| {
6323                self.transport_manager
6324                    .get_available_transports()
6325                    .contains_key(t)
6326            })
6327        });
6328        if let Ok(state) = lock_shared_state(&self.shared_state) {
6329            // Reported to telemetry only for the app's own messages, as on
6330            // the pending-ACK path; see [`OutboxEntry::app_send`].
6331            let event = Event::message_delivered(message_id.clone(), latency, hop_count, transport);
6332            state.emit_settlement(event, app_send);
6333            drop(state);
6334        } else {
6335            error!("Failed to lock shared state for an ACK event, skipping event emission");
6336        }
6337        // The peer answered, so the rest of their waiting traffic can go now
6338        // rather than waiting out its own backoff or escalated probe timers.
6339        self.flush_outbox_for_peer_via(&recipient, redrive_via);
6340    }
6341
6342    pub(super) fn handle_missing_outbox_entry(
6343        &mut self,
6344        message_id: &MessageId,
6345        retry_count: u32,
6346    ) -> Result<()> {
6347        let state = lock_shared_state(&self.shared_state).map_err(|e| {
6348            error!(
6349                "Failed to lock shared state for missing outbox entry event: {}",
6350                e
6351            );
6352            e
6353        })?;
6354        // No entry, so no record of whether the app sent it: the app hears
6355        // about it and telemetry does not, the rule for a delivery whose
6356        // origin is unknown too.
6357        state.emit_event_to_app(Event::message_failed(
6358            message_id.clone(),
6359            "Message missing from outbox (cannot retry)",
6360            retry_count,
6361        ));
6362        drop(state);
6363
6364        self.retire_undeliverable_message(message_id, "missing outbox entry");
6365        Ok(())
6366    }
6367
6368    // ========================================================================
6369    // EVENTS
6370    // ========================================================================
6371
6372    /// Emits a message sent event.
6373    pub(super) fn emit_message_sent_event(&self, message: &Message) -> Result<()> {
6374        let state = lock_shared_state(&self.shared_state).map_err(|e| {
6375            error!("Failed to lock shared state for message sent event: {}", e);
6376            e
6377        })?;
6378        state.emit_event(Event::message_sent(message));
6379        drop(state);
6380        Ok(())
6381    }
6382
6383    /// Emits a transport switched event if the transport changed.
6384    pub(super) fn emit_transport_switch_event(
6385        &self,
6386        previous_transport: Option<TransportType>,
6387        current_transport: Option<TransportType>,
6388    ) -> Result<()> {
6389        if current_transport != previous_transport {
6390            if let Some(new_transport) = current_transport {
6391                let state = lock_shared_state(&self.shared_state).map_err(|e| {
6392                    error!(
6393                        "Failed to lock shared state for transport switch event: {}",
6394                        e
6395                    );
6396                    e
6397                })?;
6398                state.emit_event(Event::transport_switched(
6399                    previous_transport,
6400                    new_transport,
6401                    "DORS selected better transport".to_string(),
6402                ));
6403                drop(state);
6404            }
6405        }
6406        Ok(())
6407    }
6408
6409    // ========================================================================
6410    // SOCIAL
6411    // ========================================================================
6412
6413    /// Sends a connection request to another user via any available transport.
6414    ///
6415    /// The request is routed through DORS, so it works over Internet, BLE, or WiFi Direct.
6416    ///
6417    /// # Arguments
6418    ///
6419    /// * `recipient` - The user ID of the recipient
6420    /// * `sender_name` - Display name of the sender
6421    /// * `key_package` - Optional MLS key package for encrypted session setup
6422    /// * `initial_message` - Optional first message shown with the request
6423    ///   (surfaced verbatim in the recipient's `ConnectionRequestReceived`
6424    ///   event; apps typically seed the conversation with it on accept).
6425    ///   At most 4096 UTF-8 bytes (`MAX_INITIAL_MESSAGE_BYTES`, crate-private)
6426    ///   — longer input is rejected with `Error::InvalidArgument`.
6427    ///
6428    /// # Encryption
6429    ///
6430    /// Connection requests are internal control messages sent in plaintext,
6431    /// exempt from `require_encryption` (same as key packages and welcome
6432    /// messages) — an `initial_message` is therefore NOT end-to-end
6433    /// encrypted and should be treated like the sender display name.
6434    pub fn send_connection_request(
6435        &mut self,
6436        recipient: &str,
6437        sender_name: &str,
6438        key_package: Option<Vec<u8>>,
6439        initial_message: Option<String>,
6440    ) -> Result<MessageId> {
6441        Self::validate_outbound_recipient(recipient)?;
6442        if let Some(msg) = initial_message.as_deref() {
6443            if msg.len() > MAX_INITIAL_MESSAGE_BYTES {
6444                return Err(Error::InvalidArgument(format!(
6445                    "initial_message exceeds {} bytes (got {})",
6446                    MAX_INITIAL_MESSAGE_BYTES,
6447                    msg.len()
6448                )));
6449            }
6450        }
6451
6452        // Connection requests are internal control messages (not user content),
6453        // so they are exempt from require_encryption — same as key packages.
6454        if self.is_user_blocked(recipient) {
6455            return Err(Error::UserBlocked(recipient.to_string()));
6456        }
6457
6458        let payload = ConnectionRequestPayload {
6459            sender_name: sender_name.to_string(),
6460            timestamp_ms: Utc::now().timestamp_millis(),
6461            key_package,
6462            initial_message,
6463        };
6464
6465        let serialized =
6466            serde_json::to_string(&payload).map_err(|e| Error::Serialization(e.to_string()))?;
6467        let content = format!("{}{}", internal_prefixes::CONN_REQUEST, serialized);
6468
6469        let message_id =
6470            self.send_internal_message_alerting(recipient, content, MessagePriority::High, true)?;
6471        self.track_pending_connection_request(&message_id, recipient);
6472        info!(recipient = %recipient, "Sent connection request");
6473        Ok(message_id)
6474    }
6475
6476    /// Records an outbound connection request so a later transport-level
6477    /// "recipient offline" verdict can be surfaced as a typed event
6478    /// (see `on_transport_send_failed`). TTL-prunes and caps the map here,
6479    /// on the write path, so it needs no periodic sweep.
6480    fn track_pending_connection_request(&mut self, message_id: &MessageId, recipient: &str) {
6481        let now = std::time::Instant::now();
6482        self.pending_connection_requests
6483            .retain(|_, p| now.duration_since(p.sent_at) <= PENDING_CONNECTION_REQUEST_TTL);
6484        if self.pending_connection_requests.len() >= MAX_PENDING_CONNECTION_REQUESTS {
6485            if let Some(oldest) = self
6486                .pending_connection_requests
6487                .iter()
6488                .min_by_key(|(_, p)| p.sent_at)
6489                .map(|(id, _)| id.clone())
6490            {
6491                self.pending_connection_requests.remove(&oldest);
6492            }
6493        }
6494        self.pending_connection_requests.insert(
6495            message_id.as_str(),
6496            PendingConnectionRequest {
6497                recipient: recipient.to_string(),
6498                sent_at: now,
6499            },
6500        );
6501    }
6502
6503    /// Accepts a connection request from another user via any available transport.
6504    ///
6505    /// The response is routed through DORS, so it works over Internet, BLE, or WiFi Direct.
6506    ///
6507    /// # Arguments
6508    ///
6509    /// * `recipient` - The user ID of the original requester
6510    /// * `accepter_name` - Display name of the accepting party
6511    /// * `key_package` - Optional MLS key package for encrypted session setup
6512    ///
6513    /// # Encryption
6514    ///
6515    /// Connection accepts are internal control messages sent in plaintext,
6516    /// exempt from `require_encryption` (same as key packages and welcome messages).
6517    pub fn accept_connection_request(
6518        &mut self,
6519        recipient: &str,
6520        accepter_name: &str,
6521        key_package: Option<Vec<u8>>,
6522    ) -> Result<MessageId> {
6523        Self::validate_outbound_recipient(recipient)?;
6524        // Accept messages are internal control messages (not user content),
6525        // so they are exempt from require_encryption — same as key packages.
6526        if self.is_user_blocked(recipient) {
6527            return Err(Error::UserBlocked(recipient.to_string()));
6528        }
6529
6530        let payload = ConnectionAcceptedPayload {
6531            accepted_by_name: accepter_name.to_string(),
6532            timestamp_ms: Utc::now().timestamp_millis(),
6533            key_package,
6534        };
6535
6536        let serialized =
6537            serde_json::to_string(&payload).map_err(|e| Error::Serialization(e.to_string()))?;
6538        let content = format!("{}{}", internal_prefixes::CONN_ACCEPT, serialized);
6539
6540        let message_id =
6541            self.send_internal_message_alerting(recipient, content, MessagePriority::High, true)?;
6542        info!(recipient = %recipient, "Accepted connection request");
6543        Ok(message_id)
6544    }
6545
6546    /// Rejects a connection request from another user via any available transport.
6547    ///
6548    /// The response is routed through DORS, so it works over Internet, BLE, or WiFi Direct.
6549    ///
6550    /// # Arguments
6551    ///
6552    /// * `recipient` - The user ID of the original requester
6553    ///
6554    /// # Encryption
6555    ///
6556    /// Connection rejects are internal control messages sent in plaintext,
6557    /// exempt from `require_encryption` (same as key packages and welcome messages).
6558    pub fn reject_connection_request(&mut self, recipient: &str) -> Result<MessageId> {
6559        Self::validate_outbound_recipient(recipient)?;
6560        // Reject messages are internal control messages (not user content),
6561        // so they are exempt from require_encryption — same as key packages.
6562        if self.is_user_blocked(recipient) {
6563            return Err(Error::UserBlocked(recipient.to_string()));
6564        }
6565
6566        let content = internal_prefixes::CONN_REJECT.to_string();
6567
6568        let message_id = self.send_internal_message(recipient, content, MessagePriority::High)?;
6569        info!(recipient = %recipient, "Rejected connection request");
6570        Ok(message_id)
6571    }
6572
6573    /// Cancels a previously sent connection request via any available transport.
6574    ///
6575    /// The cancellation is routed through DORS, so it works over Internet, BLE, or WiFi Direct.
6576    ///
6577    /// # Arguments
6578    ///
6579    /// * `recipient` - The user ID of the original request recipient
6580    ///
6581    /// # Encryption
6582    ///
6583    /// Connection cancellations are internal control messages sent in plaintext,
6584    /// exempt from `require_encryption` (same as key packages and welcome messages).
6585    pub fn cancel_connection_request(&mut self, recipient: &str) -> Result<MessageId> {
6586        Self::validate_outbound_recipient(recipient)?;
6587        if self.is_user_blocked(recipient) {
6588            return Err(Error::UserBlocked(recipient.to_string()));
6589        }
6590
6591        let content = internal_prefixes::CONN_CANCEL.to_string();
6592
6593        let message_id = self.send_internal_message(recipient, content, MessagePriority::High)?;
6594        info!(recipient = %recipient, "Cancelled connection request");
6595        Ok(message_id)
6596    }
6597
6598    /// Sends a presence update to a single peer (unicast).
6599    ///
6600    /// This sends to one recipient at a time. To broadcast presence to multiple
6601    /// peers, the caller must invoke this method once per peer.
6602    ///
6603    /// # Arguments
6604    ///
6605    /// * `recipient` - The user ID of the peer to notify
6606    /// * `status` - Presence status
6607    pub fn send_presence_update(
6608        &mut self,
6609        recipient: &str,
6610        status: PresenceStatus,
6611    ) -> Result<MessageId> {
6612        Self::validate_outbound_recipient(recipient)?;
6613        if self.is_user_blocked(recipient) {
6614            return Err(Error::UserBlocked(recipient.to_string()));
6615        }
6616
6617        let payload = PresencePayload {
6618            status,
6619            timestamp_ms: Utc::now().timestamp_millis(),
6620        };
6621
6622        let serialized =
6623            serde_json::to_string(&payload).map_err(|e| Error::Serialization(e.to_string()))?;
6624        let content = format!("{}{}", internal_prefixes::PRESENCE, serialized);
6625
6626        let message_id = self.send_internal_message(recipient, content, MessagePriority::Low)?;
6627        debug!(recipient = %recipient, status = ?status, "Sent presence update");
6628        Ok(message_id)
6629    }
6630
6631    /// Sends a typing indicator to a peer.
6632    ///
6633    /// # Arguments
6634    ///
6635    /// * `recipient` - The user ID of the peer to notify
6636    /// * `conversation_id` - Opaque conversation key, carried to the peer and
6637    ///   echoed back on its event. Never parsed or routed on. Conventionally
6638    ///   the peer's address for a DM and the group id for a group; any stable
6639    ///   string works, but a display name does not (those change).
6640    /// * `is_typing` - Whether the user started or stopped typing
6641    pub fn send_typing_indicator(
6642        &mut self,
6643        recipient: &str,
6644        conversation_id: &str,
6645        is_typing: bool,
6646    ) -> Result<MessageId> {
6647        Self::validate_outbound_recipient(recipient)?;
6648        if self.is_user_blocked(recipient) {
6649            return Err(Error::UserBlocked(recipient.to_string()));
6650        }
6651        if conversation_id.is_empty() {
6652            return Err(Error::InvalidArgument(
6653                "conversation_id must not be empty".to_string(),
6654            ));
6655        }
6656
6657        let payload = TypingIndicatorPayload {
6658            conversation_id: conversation_id.to_string(),
6659            is_typing,
6660            timestamp_ms: Utc::now().timestamp_millis(),
6661        };
6662
6663        let serialized =
6664            serde_json::to_string(&payload).map_err(|e| Error::Serialization(e.to_string()))?;
6665        let content = format!("{}{}", internal_prefixes::TYPING_INDICATOR, serialized);
6666
6667        let message_id = self.send_internal_message(recipient, content, MessagePriority::Low)?;
6668        debug!(recipient = %recipient, is_typing = %is_typing, "Sent typing indicator");
6669        Ok(message_id)
6670    }
6671
6672    /// Sends a read receipt to a peer, indicating that the given messages have been read.
6673    ///
6674    /// # Arguments
6675    ///
6676    /// * `recipient` - The user ID of the peer who sent the messages
6677    /// * `message_ids` - IDs of the messages that were read (max 256)
6678    pub fn send_read_receipt(
6679        &mut self,
6680        recipient: &str,
6681        message_ids: Vec<String>,
6682    ) -> Result<MessageId> {
6683        Self::validate_outbound_recipient(recipient)?;
6684        if self.is_user_blocked(recipient) {
6685            return Err(Error::UserBlocked(recipient.to_string()));
6686        }
6687        if message_ids.is_empty() {
6688            return Err(Error::InvalidArgument(
6689                "message_ids must not be empty".to_string(),
6690            ));
6691        }
6692        if message_ids.len() > MAX_READ_RECEIPT_IDS {
6693            return Err(Error::InvalidArgument(format!(
6694                "message_ids exceeds maximum of {MAX_READ_RECEIPT_IDS}"
6695            )));
6696        }
6697
6698        let payload = ReadReceiptPayload {
6699            message_ids,
6700            timestamp_ms: Utc::now().timestamp_millis(),
6701        };
6702
6703        let serialized =
6704            serde_json::to_string(&payload).map_err(|e| Error::Serialization(e.to_string()))?;
6705        let content = format!("{}{}", internal_prefixes::READ_RECEIPT, serialized);
6706
6707        let message_id = self.send_internal_message(recipient, content, MessagePriority::Low)?;
6708        debug!(recipient = %recipient, "Sent read receipt");
6709        Ok(message_id)
6710    }
6711
6712    /// Builds the advertisement body carried by a key package, whether it is
6713    /// pushed to a peer or published for a stranger to fetch.
6714    ///
6715    /// Shared so the two paths cannot drift: a published record that advertised
6716    /// a different capability set from a pushed one would make a peer's
6717    /// behaviour depend on which way they first met us.
6718    pub(crate) fn build_key_package_payload(
6719        &self,
6720        key_pkg: &offline_protocol_mls::KeyPackageBundle,
6721        session_reset: bool,
6722    ) -> KeyPackagePayload {
6723        KeyPackagePayload {
6724            user_id: self.local_id.clone(),
6725            key_package_data: key_pkg.key_package_data.clone(),
6726            remaining_lifetime_ms: key_pkg.remaining_lifetime_ms(),
6727            timestamp_ms: Utc::now().timestamp_millis() as u64,
6728            session_reset,
6729            wire_versions: if self.config.transport.binary_wire_enabled {
6730                vec![offline_protocol_core::WIRE_VERSION_V1]
6731            } else {
6732                Vec::new()
6733            },
6734            env_versions: if self.config.encryption.compact_envelope_enabled {
6735                vec![MLS_ENVELOPE_COMPACT_V1]
6736            } else {
6737                Vec::new()
6738            },
6739            rich_versions: if self.config.encryption.rich_payload_enabled {
6740                vec![RICH_PAYLOAD_V1]
6741            } else {
6742                Vec::new()
6743            },
6744            data_versions: self.advertised_data_versions(),
6745            // Advertised unconditionally, including with freshness
6746            // enforcement switched off. The two are different questions: this
6747            // field says which signatures we can *read*, which is always both
6748            // of them, while the kill switch decides whether a readable
6749            // signature is also refused for being stale. Withholding it under
6750            // the switch would make peers sign the older payload at us, and
6751            // the peer-capability record that closes the downgrade could then
6752            // never be written.
6753            ctrl_versions: vec![offline_protocol_sealed::CTRL_SIGN_V2],
6754            // Advertised under the switch only: a peer told we route the
6755            // sealed form seals toward us, and with the switch off we still
6756            // route it (parsing is unconditional), so this gates only whether
6757            // peers are invited to.
6758            svc_versions: if self.config.encryption.encrypt_service_messages {
6759                vec![offline_protocol_sealed::SVC_SEALED_V1]
6760            } else {
6761                Vec::new()
6762            },
6763            // Present only when the Nostr transport is installed. Advertised
6764            // regardless of the sealing kill switch, which gates what *we*
6765            // publish, not what a peer may seal to us: withholding it would
6766            // downgrade the peer's traffic to the bootstrap key as a side
6767            // effect of a local setting.
6768            // Read unconditionally: an `undecryptable` ACK is settled as "not
6769            // delivered" whatever our config, so a peer may always send us one,
6770            // and a coalesced one settles every id it names.
6771            ack_versions: vec![
6772                offline_protocol_sealed::ACK_UNDECRYPTABLE_V1,
6773                offline_protocol_sealed::ACK_COALESCED_V1,
6774            ],
6775            nostr_pubkey: self.transport_manager.nostr_public_key(),
6776        }
6777    }
6778
6779    /// Advertises this device's key package to `peer_id`.
6780    ///
6781    /// The package comes from the peer-keyed pool
6782    /// ([`MlsManager::take_push_key_package`]): `peer_id` gets its own init key,
6783    /// re-handed on every push until a Welcome consumes it. Before that, this
6784    /// path handed the *same* package to every peer until somebody used it,
6785    /// which both weakened forward secrecy at session establishment (one
6786    /// compromised init key opens every Welcome built against it) and made the
6787    /// second peer's Welcome permanently unprocessable.
6788    ///
6789    /// A peer no carrier is known to reach is reached through the mesh: the
6790    /// package is handed to neighbours to carry, as an ordinary message is
6791    /// (see [`Self::send_or_carry_handshake`]). Without that a pair whose only path
6792    /// runs through a third device could never start a session, because the
6793    /// package and the Welcome were the two frames that never left the
6794    /// device by any other route, and every frame after them is sealed.
6795    pub(crate) fn send_key_package_to(&mut self, peer_id: &str, session_reset: bool) -> Result<()> {
6796        let mls = self.mls_manager.as_ref().ok_or(Error::MlsNotInitialized)?;
6797
6798        let push = {
6799            let manager = mls
6800                .read()
6801                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
6802            manager.take_push_key_package(peer_id)?
6803        };
6804
6805        if push.pool_exhausted {
6806            self.warn_push_key_package_pool_exhausted(peer_id);
6807        }
6808
6809        let key_pkg = push.bundle;
6810        let payload = self.build_key_package_payload(&key_pkg, session_reset);
6811
6812        let serialized =
6813            serde_json::to_string(&payload).map_err(|e| Error::Serialization(e.to_string()))?;
6814        let content = format!("{}{}", internal_prefixes::KEY_PACKAGE, serialized);
6815
6816        let mut message =
6817            self.create_message(peer_id, content, Some(MessagePriority::Low), None)?;
6818        self.sign_control_message(&mut message)?;
6819
6820        // A package no carrier is known to reach the peer with may still
6821        // reach it through a neighbour. Handing it over counts as sending it:
6822        // the stamp below is what starts the re-push ladder, which is the only
6823        // repeat a package carried this way gets, since a peer we never hear
6824        // directly produces no discovery to push again on.
6825        match self.send_or_carry_handshake(&message) {
6826            Ok(_) => {
6827                // SECURITY (resource exhaustion): `key_package_sent_to` is keyed
6828                // by the wire-claimed peer id, so a forged-sender key-package
6829                // flood (each reply routes through here) would grow it without
6830                // bound. Reset at capacity like `plaintext_receive_warned` — the
6831                // only cost of forgetting a peer is one idempotent re-send.
6832                if !self.key_package_sent_to.contains_key(peer_id)
6833                    && self.key_package_sent_to.len() >= MAX_KEY_PACKAGE_SENT_TO
6834                {
6835                    self.key_package_sent_to.clear();
6836                }
6837                // Re-stamped, and the repeat count (the re-arm's backoff) is
6838                // kept: only a reset push, which opens a new exchange, or a
6839                // removal (session reset, neighbour lost) starts it over.
6840                let now = Instant::now();
6841                let entry = self
6842                    .key_package_sent_to
6843                    .entry(peer_id.to_string())
6844                    .or_insert((now, 0));
6845                entry.0 = now;
6846                if session_reset {
6847                    entry.1 = 0;
6848                }
6849                debug!(peer_id = %peer_id, message_id = %message.id, "Sent key package");
6850                Ok(())
6851            }
6852            Err(err) => {
6853                // Don't mark as sent and don't enqueue for retry -- if the peer is
6854                // unreachable now, on_neighbor_discovered will fire again when they
6855                // reconnect, generating a fresh exchange, and a peer only the mesh
6856                // reaches is pushed to from the reconciliation tick while a
6857                // message waits for it.
6858                debug!(peer_id = %peer_id, error = %err, "Key package send deferred");
6859                Err(err)
6860            }
6861        }
6862    }
6863
6864    // ========================================================================
6865    // TRANSPORT HELPERS
6866    // ========================================================================
6867
6868    pub(super) fn transport_from_label(label: &str) -> TransportType {
6869        TransportType::from_label(label)
6870    }
6871
6872    pub(super) fn transport_label(transport: TransportType) -> &'static str {
6873        transport.label()
6874    }
6875
6876    /// The carrier a media transfer to `recipient` is pinned to, for every
6877    /// chunk and every retry.
6878    ///
6879    /// The peer stream is eligible only while a stream has proved `recipient`
6880    /// itself. The stream carries one peer, so pinned toward anyone else it
6881    /// refuses every chunk; the refused chunks, sized and windowed for a
6882    /// stream, are then offered to the mesh and reach a Bluetooth LE
6883    /// neighbour with no window at all, and the ones past the own-send budget
6884    /// retry against the pin until the transfer fails.
6885    pub(super) fn select_media_transport(&self, recipient: &str) -> Result<TransportType> {
6886        let available = self.transport_manager.get_available_transports();
6887        let eligible = |transport: TransportType| {
6888            available.contains_key(&transport)
6889                && (transport != TransportType::WiFiDirect
6890                    || self
6891                        .transport_manager
6892                        .holds_mesh_link(TransportType::WiFiDirect, recipient))
6893        };
6894
6895        if let Some(current) = self.transport_manager.current_transport() {
6896            if eligible(current) {
6897                return Ok(current);
6898            }
6899        }
6900
6901        // The daemon carrier is left out of the fallback order: its reference
6902        // backbone is scarce-class (gateway-contract.md, rate class).
6903        for preferred in [
6904            TransportType::Internet,
6905            TransportType::WiFiDirect,
6906            TransportType::BLE,
6907        ] {
6908            if eligible(preferred) {
6909                return Ok(preferred);
6910            }
6911        }
6912
6913        Err(Error::Other(
6914            "No available transport for media transfer".to_string(),
6915        ))
6916    }
6917
6918    pub(super) fn pinned_media_transport_for_message(
6919        &self,
6920        message_id: &MessageId,
6921    ) -> Option<TransportType> {
6922        let (file_id, _) = self.outbound_media_chunks.get(message_id)?;
6923        self.outbound_media_transfers
6924            .get(file_id)
6925            .map(|transfer| transfer.pinned_transport)
6926    }
6927
6928    pub(super) fn decryption_failure_code_from_kind(
6929        kind: DecryptionFailureKind,
6930    ) -> DecryptionFailureCode {
6931        match kind {
6932            DecryptionFailureKind::NotInitialized => DecryptionFailureCode::NotInitialized,
6933            DecryptionFailureKind::InvalidCiphertext => DecryptionFailureCode::InvalidCiphertext,
6934            DecryptionFailureKind::IdentityMismatch => DecryptionFailureCode::IdentityMismatch,
6935            DecryptionFailureKind::CryptoFailure => DecryptionFailureCode::CryptoFailure,
6936            DecryptionFailureKind::SessionNotFound | DecryptionFailureKind::Unknown => {
6937                DecryptionFailureCode::Unknown
6938            }
6939        }
6940    }
6941}