Skip to main content

offline_protocol/protocol/
mod.rs

1//! Main protocol engine.
2
3mod blocking;
4mod config_accessors;
5mod custodian;
6pub(crate) mod custody;
7#[cfg(feature = "data")]
8pub(crate) mod data;
9#[cfg(feature = "data")]
10mod data_sync;
11mod decryption_queue;
12mod ephemeral;
13pub(crate) mod mesh_relay;
14mod message_dispatch;
15mod nostr_discovery;
16mod nostr_publication;
17mod observability;
18mod pending_queue;
19mod prefixes;
20pub(crate) mod reachability;
21mod receive;
22mod security;
23mod send;
24mod service_seal;
25mod session;
26pub(crate) mod state_crypto;
27mod storage;
28mod types;
29
30pub use custody::{CustodyRefusal, CustodyStats};
31pub(crate) use decryption_queue::PendingDecryptionQueue;
32pub use decryption_queue::PendingQueueMetrics;
33pub(crate) use prefixes::*;
34#[cfg(feature = "file-store")]
35pub(crate) use storage::{sealed_state_key_types, stored_state_record_key, StoredRecordKey};
36pub(crate) use storage::{PruneAllowance, RestorableRecord};
37pub(crate) use types::*;
38pub use types::{GatewayCarrier, MediaSendOptions, ProtocolState, SendMessageOptions};
39
40use crate::file_transfer::{FileTransferManager, OutboundTransferState};
41use crate::group_mesh::{MAX_RELAY_CAPABILITIES, MAX_RELAY_CAPABILITY_TOKEN_BYTES};
42use crate::mls_observability::{MlsEventEmitter, MlsEventRateLimiter, NoopMlsEventEmitter};
43use crate::telemetry::aggregator::{
44    build_metrics_frame, device_battery_from_available, diff_device_capability,
45    diff_transport_state, DeviceSnap,
46};
47#[cfg(feature = "telemetry-pipe")]
48use crate::telemetry::pipe::{
49    store::Backend, AppState, PipeParts, TelemetryHost, TelemetryPipe, TelemetryStats,
50    FINAL_FLUSH_BUDGET,
51};
52use crate::telemetry::{
53    dispatch_record, Scrubber, TelemetryConfig, TelemetryContext, TelemetryRecord, TelemetrySink,
54};
55use crate::{
56    Error, EstablishmentState, Event, ProtocolConfig, ProtocolStateStorage, Result,
57    TransportManager,
58};
59use chrono::{DateTime, Utc};
60use mesh_relay::{MeshRelayGovernor, RelayActivity};
61use offline_protocol_core::time::Instant;
62use offline_protocol_core::{LamportClock, Message, MessageId, MutexExt, Username};
63use offline_protocol_mls::{EncryptedMessage, MlsManager, MlsStorage, WelcomeMessage};
64use offline_protocol_reliability::{AckManager, Deduplicator, RetryQueue};
65use offline_protocol_router::{RelayPriority, RelayRole, TransportSelector};
66use offline_protocol_services::MeshServices;
67use offline_protocol_transport::{BleTransport, TransportStatus, TransportType};
68use std::collections::HashMap;
69use std::collections::HashSet;
70use std::collections::VecDeque;
71use std::sync::{Arc, Mutex, RwLock};
72use tracing::{debug, error, info, warn};
73use zeroize::Zeroizing;
74
75/// Returns whether `timestamp` is at least `lifetime_ms` old without relying
76/// on Chrono's panicking `DateTime - Duration` operator.
77///
78/// A lifetime too large to represent, or whose cutoff predates Chrono's
79/// calendar range, cannot expire a representable timestamp.
80fn lifetime_expired(now: DateTime<Utc>, timestamp: DateTime<Utc>, lifetime_ms: u64) -> bool {
81    let Ok(lifetime_ms) = i64::try_from(lifetime_ms) else {
82        return false;
83    };
84    now.checked_sub_signed(chrono::Duration::milliseconds(lifetime_ms))
85        .is_some_and(|cutoff| timestamp <= cutoff)
86}
87
88/// Whether a relay-pushed outbox entry has outlived its fixed redelivery
89/// window ([`offline_protocol_reliability::constants::REDELIVERY_WINDOW_MS`]
90/// from `first_sent_at`). Neither the sliding window nor the restore refresh
91/// moves this: every re-send past it would earn a second visible push, and
92/// the recipient no longer recognises the id as a duplicate.
93fn relay_push_window_closed(now: DateTime<Utc>, entry: &OutboxEntry) -> bool {
94    entry.relay_pushed
95        && lifetime_expired(
96            now,
97            entry.first_sent_at,
98            offline_protocol_reliability::constants::REDELIVERY_WINDOW_MS,
99        )
100}
101
102/// Main entry point for the Offline Protocol SDK.
103///
104/// This struct combines all protocol components and provides a unified API
105/// for sending/receiving messages with automatic transport selection and
106/// reliable delivery.
107pub struct OfflineProtocol {
108    /// Configuration.
109    pub(crate) config: ProtocolConfig,
110
111    /// This device's identity on the wire: the address derived from the
112    /// identity key in this profile's storage.
113    ///
114    /// Every outbound frame is stamped with it, every self-filter compares
115    /// against it, and it is one half of every 1:1 session slot. It is set
116    /// once, by `initialize_mls`, from the key that storage holds — the
117    /// protocol cannot know it before then, because storage is what carries
118    /// the key and storage does not arrive until that call.
119    ///
120    /// Until then it holds [`ProtocolConfig::profile`], which is *not* an
121    /// address and is never a peer's real identity. That fallback keeps the
122    /// pre-MLS surface (transport wiring, local bookkeeping) working on an
123    /// instance that has not been initialized.
124    ///
125    /// One configuration can put the fallback on the wire: the explicit
126    /// `require_encryption = false` opt-out lets a send go out in plaintext
127    /// with MLS never initialized, and such a frame is stamped with the
128    /// profile. It is the same fall-through that already emits
129    /// [`crate::events::SecurityWarningCode::PlaintextSend`], so the condition
130    /// is reported rather than silent. The default config forbids it —
131    /// `require_encryption` is `true`, which fails the send closed.
132    pub(crate) local_id: String,
133
134    /// Whether [`Self::local_id`] is the derived address rather than the
135    /// profile fallback.
136    pub(crate) identity_established: bool,
137
138    /// Transport manager (manages all transports with DORS).
139    pub(crate) transport_manager: TransportManager,
140
141    /// ACK manager for tracking acknowledgments.
142    ack_manager: AckManager,
143
144    /// Retry queue for failed messages.
145    retry_queue: RetryQueue,
146
147    /// Deduplicator for preventing duplicates.
148    pub(crate) deduplicator: Deduplicator,
149
150    /// Decides which frames this device carries for other people, when they
151    /// go out, and how fast. Kept separate from [`Self::deduplicator`], whose
152    /// entries track messages addressed to us and are removed again when
153    /// delivery has to be retried.
154    pub(crate) mesh_relay: MeshRelayGovernor,
155
156    /// Frames held in custody for a neighbour, with the quotas and the
157    /// counters (`docs/spec/custody.md`). Disjoint from
158    /// [`Self::mesh_relay`]'s suppression cache by construction: accepting a
159    /// frame never records its id there, and redelivering one never consults
160    /// it, so a custodian never blanks the route it is holding.
161    pub(crate) custody: custody::CustodyStore,
162
163    /// Receipts this device holds as a *depositor*: for each outbox entry,
164    /// the custodians holding it and until when a further deposit request
165    /// toward each is suppressed, in Unix milliseconds from the receipt's own
166    /// timestamp. In memory only, bounded by the outbox and
167    /// by [`custody::MAX_CUSTODY_RECEIPTS_PER_MESSAGE`]; losing it at a
168    /// restart costs one duplicate deposit, which the custodian absorbs.
169    pub(crate) custody_receipts: HashMap<MessageId, HashMap<String, i64>>,
170
171    /// When the custody store was last swept for expired records.
172    custody_last_sweep: Instant,
173
174    /// Shared mutable state.
175    shared_state: Arc<Mutex<SharedState>>,
176
177    /// Messages awaiting delivery/acknowledgment (store-and-forward outbox).
178    outbox: HashMap<MessageId, OutboxEntry>,
179
180    /// Transient staging for outbox re-seal provenance, keyed by message id.
181    /// Populated at send time (where the plaintext is in scope) and consumed
182    /// when the outbox entry is first created (which happens deep in the send/
183    /// retry machinery, from the already-sealed `Message`). Once consumed it
184    /// lives on the `OutboxEntry` itself. See [`OutboxReseal`].
185    pending_reseal: HashMap<MessageId, OutboxReseal>,
186    /// The message the app send path is dispatching right now, read by the two
187    /// places an outbox entry is created so the entry carries
188    /// [`OutboxEntry::app_send`] from its first write to storage. Set around
189    /// the dispatch and restored to its previous value afterwards, so it never
190    /// outlives one call and a nested dispatch cannot clear an outer one's.
191    ///
192    /// Staged rather than set on the entry afterwards: an entry is persisted
193    /// when it is created and not again on later attempts, so a flag set after
194    /// creation stayed in memory, and every app message restored after a
195    /// restart came back as not the app's and dropped out of telemetry.
196    staged_app_send: Option<MessageId>,
197
198    /// Outbox ids already re-sealed and resent once in answer to an
199    /// `undecryptable` ACK. A second such answer leaves the entry to its retry
200    /// ladder, so two devices cannot trade undecryptable ACKs and resends at
201    /// network speed. Cleared with the outbox entry.
202    undecryptable_resent: HashSet<MessageId>,
203
204    /// Dedicated outbox for file chunk messages, separate from the main outbox
205    /// to prevent large file transfers from evicting regular messages.
206    media_outbox: HashMap<MessageId, OutboxEntry>,
207
208    /// Outbox ids held back by the per-peer send window, per recipient, in
209    /// the order the app sent them. Sent oldest first as the peer's
210    /// acknowledgements free the window (`flush_peer_windows`). An id whose
211    /// outbox entry is gone (settled, expired, evicted) is skipped and dropped
212    /// there, so no removal path has to know about this. In-memory only: a
213    /// restart re-drives the outbox, and the gate holds it again.
214    window_held: HashMap<String, VecDeque<MessageId>>,
215
216    /// Consecutive unreachable-park counter per recipient for plain DMs,
217    /// driving the escalating reachability-probe interval on every carrier
218    /// (see `handle_recipient_unreachable_for_message` —
219    /// mirrors `WelcomeLifecycleRecord::unreachable_parks`). Reset on every
220    /// reachability edge (`flush_outbox_for_peer_via` / `flush_outbox_all`) and
221    /// on delivery; pruned alongside `cleanup_outbox` so only peers that
222    /// still hold outbox entries can retain a counter. While live, ACK
223    /// exhaustion for the recipient's plain DMs re-parks instead of
224    /// settling terminally (`try_repark_exhausted_dm`). In-memory only: a
225    /// restart re-drives the outbox anyway, which is itself a fresh probe.
226    dm_unreachable_parks: HashMap<String, u32>,
227
228    /// Delivery acknowledgements no carrier could take when they were owed,
229    /// with the transport the acknowledged frame arrived on and when they
230    /// were held. Re-routed on the next carrier-level edge
231    /// (`flush_unrouted_acks` from `flush_outbox_all`); bounded by
232    /// `MAX_UNROUTED_ACKS` and `UNROUTED_ACK_TTL`. In-memory only.
233    unrouted_acks: VecDeque<(Message, TransportType, Instant)>,
234
235    /// Per-recipient reachability claims, keyed by recipient and carrier.
236    ///
237    /// The generalisation of the counter above: that one records *how often*
238    /// a recipient has been parked, this one records *what a carrier said*
239    /// about reaching them, so a decision can be made before the send rather
240    /// than only after it failed. Written by the same producers (a gateway
241    /// verdict, a gateway presence answer), read at the send and carrying
242    /// seams. In-memory only, and absent facts mean today's behaviour.
243    pub(crate) reachability: reachability::ReachabilityFacts,
244
245    /// Capability tokens the attached gateway daemon advertised.
246    ///
247    /// Delivered at attach, before the bridge reports the carrier available,
248    /// and cleared when the carrier drops: a stale advertisement outlives the
249    /// gateway that made it, and a reconnect may land on a different one.
250    ///
251    /// Stored and never read on a decision. The backbone behind the daemon
252    /// is the gateway's own property and reaches this device only as a
253    /// `backbone_<kind>_v1` token in this set; the set is for an application
254    /// to show, and the first production reader would turn a string set by
255    /// whoever holds the socket into a routing input (ADR 0026).
256    ///
257    /// Kept apart from `group_mesh.relay_capabilities` deliberately, though
258    /// both are capability sets from a gateway. That one gates the relay
259    /// group-broadcast path, and a token advertised by whatever box is
260    /// plugged into the venue Wi-Fi must not be able to open it.
261    gateway_capabilities: HashSet<String>,
262
263    /// MLS manager for end-to-end encryption.
264    mls_manager: Option<Arc<RwLock<MlsManager>>>,
265
266    /// Pending messages waiting for session establishment (recipient -> messages).
267    pending_encrypted_messages: HashMap<String, Vec<PendingMessage>>,
268
269    /// Terminal message settlements produced while restoring, held until the
270    /// event pipeline is live.
271    ///
272    /// Restore runs inside `initialize_mls`, which apps routinely call before
273    /// installing an event callback — so a `message_failed` emitted there would
274    /// be dropped, and the app would keep an id that never resolves. `start()`
275    /// drains this, mirroring how restored media descriptors already wait to be
276    /// announced.
277    ///
278    /// Explicitly capped at [`MAX_DEFERRED_RESTORE_SETTLEMENTS`] rather than
279    /// left to the restore caps that feed it. Those caps do bound it, but they
280    /// bound it as a *sum* across every category, and nothing drains this until
281    /// `start()` — which an app that only ever calls `initialize_mls`, or that
282    /// retries it against a failing store, may never reach.
283    /// Each settlement carries whether it reaches telemetry; see
284    /// [`Self::emit_settlement`].
285    deferred_restore_settlements: Vec<(Event, bool)>,
286
287    /// Number of settlements suppressed by the cap above, so the count is
288    /// reported even though the individual events are not.
289    suppressed_restore_settlements: usize,
290
291    /// Earliest wall-clock expiry in `pending_encrypted_messages`.
292    ///
293    /// `process()` consults this before scanning the bounded queue, avoiding an
294    /// O(N) walk on every 100 ms tick while preserving exact configured expiry.
295    /// A stale early deadline is harmless: it causes one extra scan, which then
296    /// recomputes the real minimum.
297    next_pending_message_expiry: Option<DateTime<Utc>>,
298
299    /// Key packages received but not yet used (sender_id -> package).
300    pub(crate) pending_key_packages: HashMap<String, ReceivedKeyPackage>,
301
302    /// Peers we have pushed our key package to, with when we last did and how
303    /// many times the push has been re-armed since.
304    ///
305    /// The instant is what lets a push that produced no session be re-armed
306    /// (`Self::rearm_key_package_for_peer`) instead of standing in for a
307    /// delivery it never proved; the count is that re-arm's backoff.
308    pub(crate) key_package_sent_to: HashMap<String, (Instant, u32)>,
309
310    /// All discovered/connected peers, tracked independently of encryption.
311    /// Used by service discovery to know who to broadcast queries to.
312    /// Values are last-seen instants: refreshed on every discovery signal,
313    /// swept by [`Self::prune_stale_known_peers`] after `KNOWN_PEER_TTL_SECS`,
314    /// and used to pick the least-recently-seen victim when an insert hits
315    /// `MAX_KNOWN_PEERS`.
316    known_peers: HashMap<String, Instant>,
317
318    /// Sessions confirmed established (received Welcome or successful decrypt).
319    /// Only encrypt messages when the session is confirmed to avoid race conditions.
320    confirmed_sessions: std::collections::HashSet<String>,
321
322    /// Peers that have demonstrated they run MLS, used by
323    /// [`Self::accept_plaintext_content`] to reject cleartext claiming to come
324    /// from an encryption-capable peer.
325    ///
326    /// # Why this is not `confirmed_sessions`, and not a storage read
327    ///
328    /// The gate used to ask `is_session_confirmed`, which reads the
329    /// `session_states` protocol-state record. That record lives in the
330    /// install-scoped app container, where an attacker with write access can
331    /// simply **delete** it: restore then treats absence as `Pending` (it must
332    /// — see `bootstrap_missing_session_state`), the peer drops out of
333    /// `confirmed_sessions`, and the gate opens for unauthenticated cleartext
334    /// carrying an attacker-chosen `sender`. Sealing the record does not help;
335    /// a seal authenticates bytes that are present, and this attack removes
336    /// them. So the signal has to come from somewhere the app container cannot
337    /// reach.
338    ///
339    /// Both sources here are credential-store-resident: the MLS session list
340    /// and the durable capability records
341    /// ([`storage_keys::ENCRYPTION_CAPABLE_PEERS`]). Neither is sufficient
342    /// alone. A session can exist for a peer that has never signed a control
343    /// frame we verified (it is established from key packages, and the Welcome
344    /// path marks capability without a signature), and a record can outlive the
345    /// session it was learned alongside. The union is what actually tracks
346    /// "this peer speaks MLS".
347    ///
348    /// # Monotone within a run — deliberately, and this is load-bearing
349    ///
350    /// Entries are added and **never removed on session teardown**. That looks
351    /// like a leak and is in fact the whole security property. Teardown is
352    /// reachable from an *unauthenticated* frame: an injected `__MLS_ENC__`
353    /// classifies as `SessionDesync`, `schedule_session_rekey` tears down our
354    /// session and advertises a `session_reset` key package, and the peer's
355    /// `handle_key_package_message` then deletes its own. A set that shrank on
356    /// teardown would therefore let one injected frame re-open the very gate
357    /// this field exists to hold shut — moving the attack from "needs
358    /// app-container write access" to "needs one forged packet", which is
359    /// strictly worse than the bug it fixes.
360    ///
361    /// Keeping a peer after teardown costs nothing real: a peer that spoke MLS
362    /// a moment ago is still encryption-capable, so its cleartext is still a
363    /// downgrade. The invariant to preserve is one-directional — this set may
364    /// **over**-approximate (which fails closed) but must never
365    /// **under**-approximate (which fails open). Anything that populates it may
366    /// be lossy; nothing may prune it.
367    ///
368    /// There is no longer any way out at all, and that is now *correct* rather
369    /// than merely safe. The old escape hatch (`reset_tofu_for_peer`) meant
370    /// "this identity has re-keyed, treat it as new" — a thing that could
371    /// happen when an identity was a name. Under self-certifying addressing an
372    /// address is the hash of its key, so a peer that re-keys becomes a
373    /// *different address*: nothing about the old one ever stops being true,
374    /// and forgetting it could only ever be wrong.
375    ///
376    /// Bounded by [`MAX_ENCRYPTION_CAPABLE_PEERS`], enforced as a refusal rather
377    /// than an eviction so the monotonicity above survives the cap — see
378    /// [`Self::mark_encryption_capable`].
379    encryption_capable_peers: std::collections::HashSet<String>,
380
381    /// Peers whose key package advertised the compact MLS envelope
382    /// ([`MLS_ENVELOPE_COMPACT_V1`] in `env_versions`), so
383    /// `seal_encrypted_content` may emit it instead of the legacy JSON form.
384    /// Learned from key-package exchange, persisted per peer as
385    /// [`PeerCapabilities`], and repopulated by `restore_peer_capabilities`
386    /// on `initialize_mls` — unlike the transport manager's binary-wire
387    /// registry, which stays in-memory because direct connections re-exchange
388    /// key packages anyway. Bounded like `key_package_sent_to` (it is keyed
389    /// by the wire-claimed sender).
390    peer_compact_envelope: std::collections::HashSet<String>,
391
392    /// Peers whose key package advertised that they verify the
393    /// freshness-bound control payload ([`CTRL_SIGN_V2`] in `ctrl_versions`),
394    /// so control frames addressed to them are signed under
395    /// `offline-ctrl-v2`. Same lifecycle as `peer_compact_envelope` above:
396    /// learned from key-package exchange, persisted as [`PeerCapabilities`],
397    /// restored on `initialize_mls`, bounded like `key_package_sent_to`.
398    ///
399    /// This is the **send** half, and it is a hint: forgetting a peer costs
400    /// them the older payload, which they still verify. The **accept** half is
401    /// [`Self::control_freshness_peers`], which is a ratchet and is not
402    /// allowed to forget.
403    ///
404    /// [`CTRL_SIGN_V2`]: offline_protocol_sealed::CTRL_SIGN_V2
405    peer_ctrl_freshness: std::collections::HashSet<String>,
406
407    /// Peers that read an `undecryptable` ACK as "not delivered"
408    /// ([`ACK_UNDECRYPTABLE_V1`] in `ack_versions`). Only these are answered
409    /// when a frame of theirs can never decrypt: an older SDK reads any ACK as
410    /// a delivery. Same lifecycle as `peer_ctrl_freshness` above.
411    ///
412    /// [`ACK_UNDECRYPTABLE_V1`]: offline_protocol_sealed::ACK_UNDECRYPTABLE_V1
413    pub(crate) peer_undecryptable_ack: std::collections::HashSet<String>,
414
415    /// Peers that read `ack_more` on a delivery ACK ([`ACK_COALESCED_V1`] in
416    /// `ack_versions`), so several of their frames may be answered by one ACK
417    /// frame. Same lifecycle as `peer_undecryptable_ack` above.
418    ///
419    /// [`ACK_COALESCED_V1`]: offline_protocol_sealed::ACK_COALESCED_V1
420    pub(crate) peer_coalesced_ack: std::collections::HashSet<String>,
421
422    /// Delivery ACKs waiting to be coalesced, per peer: the carrier the
423    /// answered frames arrived on, when the first was held, and the answered
424    /// ids with the first one's hop count. Sent as one frame once
425    /// `ACK_COALESCE_MAX` are held or `ACK_COALESCE_WINDOW` has passed
426    /// (`flush_coalesced_acks`). In-memory only: an ACK lost with it is
427    /// answered again when the resend arrives as a duplicate.
428    coalesced_acks_out: HashMap<String, CoalescedAcks>,
429
430    /// Peers that have presented a control-frame signature over the
431    /// freshness-bound payload, so their frames under the older one are
432    /// refused.
433    ///
434    /// A **ratchet**, and the difference from every capability set above is
435    /// the whole point: those record what a peer said and may be forgotten
436    /// harmlessly, this records what a peer *proved* and forgetting it
437    /// re-opens the replay it exists to close. It is durable (in the peer's
438    /// [`EncryptionCapableEntry`], not in [`PeerCapabilities`], which is
439    /// overwritten wholesale by every key package) and has no removal path
440    /// short of forgetting the peer entirely.
441    ///
442    /// A strict subset of [`Self::encryption_capable_peers`] by construction,
443    /// so `MAX_ENCRYPTION_CAPABLE_PEERS` bounds it too. See
444    /// `record_control_freshness_proved` for why the ordering that makes that
445    /// true is load-bearing.
446    pub(crate) control_freshness_peers: std::collections::HashSet<String>,
447
448    /// Per-peer high-water mark: the signed timestamp of the most recent
449    /// session reset acted on from that peer.
450    ///
451    /// A reset is honoured only when its signed timestamp is strictly newer
452    /// than this, which is what stops one captured reset frame from being
453    /// spent again and again inside the freshness window. Chosen over a set of
454    /// spent frame ids because it is one integer per peer instead of a list
455    /// that grows with traffic, and because it is strictly stronger: it
456    /// refuses *every* older reset, including ones this node has no memory of.
457    ///
458    /// Durable in the peer's [`EncryptionCapableEntry`], and bounded by
459    /// construction like [`Self::control_freshness_peers`]: entries are only
460    /// written for peers the capped encryption-capable set accepted.
461    pub(crate) control_reset_watermark: std::collections::HashMap<String, i64>,
462
463    /// Peers whose key package advertised the sealed rich payload
464    /// ([`RICH_PAYLOAD_V1`] in `rich_versions`), so the send path may seal
465    /// rich extras (reply context, rich media metadata, forward attribution)
466    /// inside the `__RICH_V1__` body. Same lifecycle as
467    /// `peer_compact_envelope` above: learned from key-package exchange,
468    /// persisted, restored on `initialize_mls`, bounded like
469    /// `key_package_sent_to`. Forgetting a peer only costs silently dropped
470    /// rich extras — never a cleartext fallback.
471    peer_rich_payload: std::collections::HashSet<String>,
472
473    /// Peers that route a sealed service frame ([`SVC_SEALED_V1`] in
474    /// `svc_versions`), so a service request or response addressed to them
475    /// may travel inside `__MLS_ENC__` rather than as signed plaintext.
476    ///
477    /// The **advertised** half: learned only from a **signed** key package,
478    /// like the sets above (persisted as [`PeerCapabilities`], restored on
479    /// `initialize_mls`), and rewritten by every key package, so a fresh one
480    /// that stops advertising removes the peer. A strict subset of
481    /// [`Self::encryption_capable_peers`], refused at its cap rather than
482    /// cleared when full: clearing would send every established peer's
483    /// service bodies in plaintext on a flood. The proved half is
484    /// [`Self::svc_sealed_proved_peers`], and either one is enough to seal.
485    ///
486    /// [`SVC_SEALED_V1`]: offline_protocol_sealed::SVC_SEALED_V1
487    pub(crate) peer_svc_sealed: std::collections::HashSet<String>,
488
489    /// Peers that have sent us a service frame sealed inside the 1:1 session,
490    /// which proves they route the sealed form.
491    ///
492    /// A **ratchet**, unlike [`Self::peer_svc_sealed`]: no key package can
493    /// remove a peer from it. The failure it prevents is a replayed key
494    /// package from before the peer upgraded, which would otherwise drop the
495    /// peer from the advertised set and send its service bodies in signed
496    /// plaintext again. A key package signed under `offline-ctrl-v1` is
497    /// admitted at any age, so nothing else bounds that replay. Durable in
498    /// the peer's [`EncryptionCapableEntry`] beside `ctrl_freshness_proved`,
499    /// and a strict subset of [`Self::encryption_capable_peers`] by
500    /// construction, so `MAX_ENCRYPTION_CAPABLE_PEERS` bounds it too.
501    ///
502    /// Knowledge, not policy: recorded whatever our own switch says, and the
503    /// switch gates only whether we seal.
504    pub(crate) svc_sealed_proved_peers: std::collections::HashSet<String>,
505
506    /// Peers whose key package advertised replicated-document sync
507    /// ([`DATA_SYNC_V1`] in `data_versions`), so the send path may push
508    /// `__DATA_V1__` frames to them. Same lifecycle as the two capability
509    /// sets above: learned from key-package exchange, persisted, restored on
510    /// `initialize_mls`, bounded like `key_package_sent_to`.
511    ///
512    /// Forgetting a peer costs replication with them until the next live
513    /// exchange, which is worse than it sounds: documents stay editable on
514    /// both sides and diverge quietly, so the symptom is not an error but
515    /// two people looking at different text. It is why this set is restored
516    /// rather than relearned.
517    ///
518    /// [`DATA_SYNC_V1`]: crate::protocol::types::DATA_SYNC_V1
519    peer_data_sync: std::collections::HashSet<String>,
520
521    /// Peers whose sealed-rich-payload support we learned *indirectly*: a
522    /// group inviter attested it on the Add commit (to existing members) or
523    /// the Welcome (to the joining member), because the members of a group
524    /// never directly exchange key packages with everyone else. Kept
525    /// separate from `peer_rich_payload` (direct self-advertisement) so
526    /// direct knowledge can stay authoritative: any directly received key
527    /// package from a peer evicts its attested entry. Consulted only by
528    /// `group_rich_seal_active` — never DM sealing (which always has direct
529    /// knowledge: session establishment exchanges key packages) and never
530    /// envelope selection (a stale attestation there could corrupt
531    /// decoding; here the worst case is one member rendering a literal
532    /// `__RICH_V1__` body). Persisted inside `PeerCapabilities`, restored
533    /// on `initialize_mls`, bounded like `key_package_sent_to`.
534    peer_rich_attested: std::collections::HashSet<String>,
535
536    /// Peers whose key package advertised *group* document replication
537    /// ([`DATA_GROUP_V1`] in `data_versions`), so a group they share with
538    /// this device may carry `__DATA_V1__` frames.
539    ///
540    /// Separate from `peer_data_sync` because the two entries mean
541    /// different things and a peer can advertise the first without the
542    /// second: an install predating group spaces replicates 1:1 perfectly
543    /// well and would render a group sync frame as chat text. Same
544    /// lifecycle as every capability set: learned from key-package
545    /// exchange, persisted, restored on `initialize_mls`, bounded like
546    /// `key_package_sent_to`.
547    ///
548    /// [`DATA_GROUP_V1`]: crate::protocol::types::DATA_GROUP_V1
549    peer_data_group: std::collections::HashSet<String>,
550
551    /// Peers whose *group* replication support we learned indirectly, from
552    /// an inviter's attestation on the Add commit or Welcome.
553    ///
554    /// The group analogue of `peer_rich_attested`, and load-bearing for the
555    /// same structural reason: members of a group never exchange key
556    /// packages with each other, so without attestation a group would
557    /// replicate only among whichever members happened to have met
558    /// directly. Direct knowledge stays authoritative — a key package from
559    /// the peer evicts its attested entry. Consulted only by
560    /// `group_data_sync_active`, never by 1:1 sync.
561    peer_data_group_attested: std::collections::HashSet<String>,
562
563    /// Peers whose key package advertised attachment and snapshot carriage
564    /// over the media path ([`DATA_MEDIA_V1`] in `data_versions`), so this
565    /// device may ask them for a blob and may hand them a document too large
566    /// to travel inside a sync frame.
567    ///
568    /// A third set rather than a flag on the first, for the same reason
569    /// `peer_data_group` is separate: a peer can advertise 1:1 replication
570    /// without this, and toward such a peer a data-purposed media transfer
571    /// arrives as an ordinary received file. Same lifecycle as every
572    /// capability set: learned from key-package exchange, persisted inside
573    /// `PeerCapabilities`, restored on `initialize_mls`, bounded like
574    /// `key_package_sent_to`.
575    ///
576    /// [`DATA_MEDIA_V1`]: crate::protocol::types::DATA_MEDIA_V1
577    peer_data_media: std::collections::HashSet<String>,
578
579    /// Peers whose key package advertised document removals
580    /// ([`DATA_TOMBSTONE_V1`] in `data_versions`), so a version offer toward
581    /// them carries the `gone` field.
582    ///
583    /// A fourth set for the same reason as the third, with one difference
584    /// worth keeping in mind: this one gates traffic, not safety. A peer
585    /// missing from it ignores the field rather than mishandling it, and
586    /// this device still refuses that peer's offers of removed content
587    /// against its own floor.
588    ///
589    /// [`DATA_TOMBSTONE_V1`]: crate::protocol::types::DATA_TOMBSTONE_V1
590    peer_data_tombstones: std::collections::HashSet<String>,
591
592    /// Peers whose key package advertised interest-scoped replication
593    /// ([`DATA_INTEREST_V1`] in `data_versions`), so a version offer toward
594    /// them carries the `want` field.
595    ///
596    /// A fifth set, and a traffic gate like the fourth: a peer missing from
597    /// it answers with everything it holds, and this device refuses what it
598    /// did not ask for on arrival.
599    ///
600    /// [`DATA_INTEREST_V1`]: crate::protocol::types::DATA_INTEREST_V1
601    peer_data_interest: std::collections::HashSet<String>,
602
603    /// Peers whose key package advertised attachment bytes inside a group
604    /// ([`DATA_GROUP_BLOB_V1`] in `data_versions`).
605    ///
606    /// Direct knowledge only. Members of a group do not exchange key
607    /// packages with each other, so absence from this set is "we have not
608    /// heard" far more often than "they cannot", which is why the attested
609    /// sibling below exists and why the refusal reads both.
610    ///
611    /// [`DATA_GROUP_BLOB_V1`]: crate::protocol::types::DATA_GROUP_BLOB_V1
612    peer_data_group_blob: std::collections::HashSet<String>,
613
614    /// Peers an inviter attested carry attachment bytes inside a group.
615    ///
616    /// The sibling of `peer_data_group_attested`, and it exists for the same
617    /// reason: this is the only way to learn anything about a member whose
618    /// key package this device has never seen. Evicted by any directly
619    /// received key package, in either direction.
620    peer_data_group_blob_attested: std::collections::HashSet<String>,
621
622    /// Peers whose key package advertised the custody receipt
623    /// ([`DATA_CUSTODY_V1`] in `data_versions`), so this device may answer
624    /// their deposits with one. Gates the receipt and nothing else: a deposit
625    /// is judged by the quotas. Persisted inside `PeerCapabilities`, restored
626    /// on `initialize_mls`, bounded like `key_package_sent_to`.
627    ///
628    /// [`DATA_CUSTODY_V1`]: crate::protocol::types::DATA_CUSTODY_V1
629    peer_data_custody: std::collections::HashSet<String>,
630
631    /// Peers already flagged with a `PlaintextSend` security warning, so the
632    /// explicit-opt-out plaintext path warns once per peer instead of once
633    /// per message.
634    pub(crate) plaintext_send_warned: std::collections::HashSet<String>,
635
636    /// Peers already flagged with a `PlaintextReceiveRejected` security
637    /// warning, so a chatty legacy or malicious peer warns once per peer
638    /// instead of once per rejected message. Keys are wire-claimed
639    /// (attacker-controllable) sender ids, so the set is bounded: it resets
640    /// at `MAX_PLAINTEXT_RECEIVE_WARNED_PEERS` instead of growing without
641    /// limit.
642    pub(crate) plaintext_receive_warned: std::collections::HashSet<String>,
643
644    /// Peers whose durable `encryption_capable_peers` record is known to be on
645    /// disk — either restored from it at boot or written successfully this run.
646    ///
647    /// Purely a write-elision cache for
648    /// [`OfflineProtocol::record_encryption_capable`]: a verified control frame
649    /// arrives on chatty prefixes (`__TYPING__`, `__PRESENCE__`), and the record
650    /// it would rewrite carries no field any decision reads. Tracking *landed
651    /// writes* rather than set membership is what keeps a best-effort storage
652    /// failure retryable on the peer's next frame instead of stranded for the
653    /// process.
654    ///
655    /// A subset of [`Self::encryption_capable_peers`] by construction, so
656    /// `MAX_ENCRYPTION_CAPABLE_PEERS` bounds it too.
657    pub(crate) encryption_capable_persisted: HashSet<String>,
658
659    /// Which control-gate rejection codes have already been reported per peer,
660    /// as a bitmask (see `OfflineProtocol::control_gate_warning_bit`), so a
661    /// peer whose control traffic keeps being refused warns once per code
662    /// instead of once per frame.
663    ///
664    /// Keys are wire-claimed (attacker-controllable) sender ids, so the map is
665    /// bounded: it resets at `MAX_CONTROL_GATE_WARNED_PEERS` instead of growing
666    /// without limit.
667    pub(crate) control_gate_warned: HashMap<String, u8>,
668
669    /// Bounded pending decryption queue for encrypted messages received before
670    /// the MLS session is ready.
671    pub(crate) pending_queue: PendingDecryptionQueue,
672
673    /// Secure storage for MLS/key material and SDK-owned secrets.
674    secure_storage: Option<Arc<dyn MlsStorage>>,
675
676    /// Install-scoped storage for restartable message-plane and protocol state.
677    protocol_state_storage: Option<Arc<dyn ProtocolStateStorage>>,
678
679    /// Seals sensitive protocol-state records before they reach the
680    /// install-scoped store, with a per-install key held in `secure_storage`.
681    ///
682    /// `None` until that key is available (and if it never becomes available,
683    /// for the whole session). Sealed categories then fail *closed*: they are
684    /// not persisted at all rather than written in the clear, since losing
685    /// crash recovery is recoverable and losing at-rest confidentiality is not.
686    /// See `state_crypto` and `restore_or_init_state_record_key`.
687    ///
688    /// Always belongs to the currently attached `secure_storage` — it is
689    /// re-derived, never carried across a storage swap.
690    state_record_cipher: Option<state_crypto::StateRecordCipher>,
691
692    /// Open replicated documents and their per-space indexes.
693    #[cfg(feature = "data")]
694    pub(crate) data: data::DataLayer,
695
696    /// When each peer was last offered our document versions.
697    ///
698    /// Neighbour rediscovery is not a rare event: a peer walking in and out
699    /// of Bluetooth range fires it repeatedly, and an offer per discovery
700    /// would put a frame on the mesh every time. The offer is not urgent
701    /// (anything committed locally is pushed as it happens) and it is not
702    /// load-bearing (the next trigger repeats it), so it is exactly the kind
703    /// of traffic that should be rate limited rather than merely small.
704    #[cfg(feature = "data")]
705    pub(crate) last_data_sync_offer: HashMap<String, Instant>,
706
707    /// Settle offers waiting for local commits to a space to go quiet, keyed
708    /// by space: when the offer is due, and the documents committed since
709    /// the last one. Pushed out by every local commit and removed when it
710    /// fires. See [`DATA_SYNC_SETTLE_DELAY`].
711    ///
712    /// [`DATA_SYNC_SETTLE_DELAY`]: crate::protocol::data_sync::DATA_SYNC_SETTLE_DELAY
713    #[cfg(feature = "data")]
714    pub(crate) data_sync_settle: HashMap<String, (Instant, std::collections::BTreeSet<String>)>,
715
716    /// Attachment fetches asked for and not yet answered, keyed by space and
717    /// blob hash, valued by the last sign that an answer is coming: the
718    /// moment the question went out, or the arrival of a chunk of its
719    /// answer.
720    ///
721    /// The refresh is what keeps this record's two jobs consistent. It bounds
722    /// how long a question is remembered, and it is also what admits the
723    /// bytes when they finally land, so a record that expired while its own
724    /// answer was arriving would discard a blob that fully crossed.
725    ///
726    /// Load-bearing for more than bookkeeping: arriving blob bytes are
727    /// admitted only against an entry here, checked when the transfer's
728    /// chunk 0 identifies it and again before the bytes reach the
729    /// application. Without it a peer could push blobs this device never
730    /// asked for and the application would be handed files nobody wanted.
731    ///
732    /// The bound it gives is on delivery and on the bulk of the buffering,
733    /// not on every byte: chunks need not arrive in order, so any that land
734    /// before chunk 0 are buffered before there is anything to judge, and
735    /// are released when it arrives. What remains is bounded by the media
736    /// layer's own resource caps, the same ones every unsolicited file
737    /// transfer has always been subject to.
738    ///
739    /// Gated on the data feature, which the record's own type now is: what
740    /// a fetch is waiting on, and what it has assembled so far, are facts
741    /// about the data layer and have no meaning without it.
742    #[cfg(feature = "data")]
743    pub(crate) pending_attachment_fetches:
744        HashMap<String, crate::protocol::data_sync::PendingFetch>,
745
746    /// Blob requests recently acted on, keyed by asking peer and blob hash.
747    ///
748    /// Its own map rather than a corner of `last_data_sync_offer`, because
749    /// this is the only rate-limit key a peer chooses freely: every other one
750    /// is a peer or a peer-and-group, bounded by a roster. Bounded here by
751    /// [`MAX_BLOB_REQUESTS_PER_WINDOW`] per peer and
752    /// [`MAX_BLOB_REQUEST_WINDOWS`] overall, and swept on every read.
753    ///
754    /// [`MAX_BLOB_REQUESTS_PER_WINDOW`]: crate::protocol::data_sync::MAX_BLOB_REQUESTS_PER_WINDOW
755    /// [`MAX_BLOB_REQUEST_WINDOWS`]: crate::protocol::data_sync::MAX_BLOB_REQUEST_WINDOWS
756    pub(crate) blob_request_windows: HashMap<String, Instant>,
757
758    /// Whether the MLS group list has been read into `group_mesh.members`
759    /// once this session, so the shared-group rediscovery sweep can see the
760    /// groups this device is in.
761    ///
762    /// That cache is filled on demand by group traffic and is empty after a
763    /// restart, so a sweep that only read it would find nothing on exactly
764    /// the launch where two members have drifted apart and neither is
765    /// editing. Enumerating is one key listing plus, per group not already
766    /// cached, the group-state read the next group send would have paid
767    /// anyway.
768    ///
769    /// One shot rather than per sweep because the cache stays complete once
770    /// filled: creating a group inserts, a Welcome inserts, and leaving
771    /// removes. Deliberately not a cold-start sweep, which was rejected for
772    /// putting an offer per member per group on the mesh at every launch;
773    /// this is per peer, only when that peer is actually in front of us, and
774    /// still behind the offer window.
775    #[cfg(feature = "data")]
776    pub(crate) group_spaces_enumerated: bool,
777
778    /// Lamport logical clock for causal message ordering.
779    /// Ticked on send, merged on receive.
780    lamport_clock: LamportClock,
781
782    /// Retry schedule for peers whose confirmation persistence failed.
783    confirmation_retry_due_at: HashMap<String, DateTime<Utc>>,
784
785    /// Probe schedule for pending sessions to guarantee post-restart convergence.
786    confirmation_probe_due_at: HashMap<String, DateTime<Utc>>,
787
788    /// Consecutive relay "recipient unreachable" verdicts per peer for the
789    /// session-confirmation probe. Drives an escalating backoff of
790    /// `confirmation_probe_due_at` (15s -> 600s cap, the same ladder the welcome
791    /// lifecycle uses) so a peer that established a session then vanished is not
792    /// probed every `CONFIRMATION_PROBE_INTERVAL_SECS` forever; a fleet of such
793    /// peers would otherwise exceed the relay's per-connection rate limit and
794    /// flap the connection. Reset on a reachability edge (`on_peer_presence`).
795    confirmation_probe_unreachable_parks: HashMap<String, u32>,
796
797    /// The confirmation probe each pending peer last sent. At most one probe
798    /// per peer lives in the outbox: a new one supersedes the last. Stacking a
799    /// fresh probe (new id, full retry ladder) every
800    /// `CONFIRMATION_PROBE_INTERVAL_SECS` on top of the unanswered ones filled
801    /// the outbox whenever no relay verdict (`unreachable` or
802    /// `relay_pushed`) arrived to back the schedule off, as on a mesh-only
803    /// carrier. Capacity eviction then failed real messages.
804    confirmation_probe_outstanding: HashMap<String, MessageId>,
805
806    /// The probe each pending peer's current one superseded. A relay verdict
807    /// finds its peer through the outbox entry, which the supersede removed,
808    /// so a verdict slower than `CONFIRMATION_PROBE_INTERVAL_SECS` would
809    /// otherwise be dropped and never back the schedule off, leaving the
810    /// peer probed every 5s against a relay that has said it is not there.
811    /// One id per peer, so this covers a verdict up to two intervals late;
812    /// one later than that is dropped, and the next probe's verdict backs
813    /// the schedule off instead.
814    confirmation_probe_superseded: HashMap<String, MessageId>,
815
816    /// Token bucket that caps how fast the RETRY/PROBE path
817    /// (`process_retry_queue`) re-sends, so a large backlog of resends to
818    /// unreachable peers cannot burst past the relay's per-connection rate limit
819    /// and trigger the disconnect->reconnect->reflood loop ("flapping"). Only
820    /// consulted when `RetryConfig::edge_driven_unreachable_dm` is opted in. It
821    /// bounds ONLY resends — first-attempt sends, ACKs, control frames and
822    /// handshakes take other paths and are never throttled, so live traffic
823    /// keeps full throughput/latency. `retry_drain_last` is None until first use.
824    retry_drain_tokens: f64,
825    retry_drain_last: Option<Instant>,
826
827    /// Rate-limit schedule for 1:1 session re-keys triggered by an epoch-desync
828    /// decrypt failure. Bounds the re-key to at most one per peer per
829    /// `REKEY_INTERVAL_SECS` so a peer replaying stale-epoch ciphertext (or an
830    /// injected wrong-epoch frame) cannot drive a re-key storm. The floor is
831    /// **never reset early** — not even by a successful decrypt on the healed
832    /// session — because a genuine re-fork and a replay are indistinguishable
833    /// here; it lapses only by the interval elapsing (see
834    /// `schedule_session_rekey`). In-memory only: a re-key is a live-connectivity
835    /// action, and a fresh desync after restart simply re-arms it.
836    rekey_due_at: HashMap<String, DateTime<Utc>>,
837
838    /// When each peer may next be asked to corroborate a session that is
839    /// failing ordinary decrypts (`request_session_corroboration`). Keyed by
840    /// the wire-claimed sender, so bounded like `rekey_due_at`. In memory
841    /// only: the next failure re-arms it.
842    corroboration_due_at: HashMap<String, DateTime<Utc>>,
843
844    /// The corroboration challenge this side has out to each peer, with the
845    /// session it was issued under (`session_challenge_body`). Only an
846    /// acknowledgement answering it under the current session may heal a
847    /// split. Bounded like `rekey_due_at`. In memory only: an answer arriving
848    /// after a restart is not acted on, and the next challenge asks again.
849    corroboration_challenges: HashMap<String, CorroborationChallenge>,
850
851    /// The signed stamp, unclamped, of the frame each peer's session was built
852    /// from, and when it was noted (`note_session_frame_stamp`). Refuses a
853    /// `no_session` teardown older than that frame for
854    /// `SESSION_BUILD_STAMP_WINDOW` after it, whatever the skew between the
855    /// two clocks. In memory only, and bounded like `rekey_due_at`.
856    session_build_stamps: HashMap<String, (i64, Instant)>,
857
858    /// When to ask each peer again for a corroboration whose heal the re-key
859    /// floor held back: the moment the floor opens. Sent from the
860    /// reconciliation tick (`send_due_corroboration_reasks`), because the ask
861    /// that drew the answer has just set its own limit and an inline ask would
862    /// do nothing. Bounded like `rekey_due_at`. In memory only.
863    corroboration_reask_at: HashMap<String, DateTime<Utc>>,
864
865    /// Outbound welcome lifecycle records keyed by peer id.
866    welcome_lifecycles: HashMap<String, WelcomeLifecycleRecord>,
867
868    /// Outbound connection requests still awaiting a transport outcome,
869    /// keyed by message id → recipient. Lets `on_transport_send_failed`
870    /// turn the relay's authoritative "recipient offline" DeliveryError
871    /// into a typed `ConnectionRequestUndeliverable` event instead of
872    /// silently discarding it (the welcome-only path never matches a
873    /// connection request's message id). Entries are dropped on emission
874    /// and on proof of delivery (the request's delivery ack, or an inbound
875    /// accept/reject from the recipient), and TTL-pruned on insert and at
876    /// read; bounded, so an app spamming requests cannot grow it
877    /// unboundedly.
878    pending_connection_requests: HashMap<String, PendingConnectionRequest>,
879
880    /// Backoff state for presence-driven welcome rescue, keyed by peer id.
881    /// Bounds how often an online-but-never-confirming peer triggers a
882    /// welcome re-arm/re-send (see `on_peer_presence`). Entries are dropped
883    /// lazily once the peer has no unconfirmed welcome left.
884    welcome_presence_rescue: HashMap<String, PresenceRescueThrottle>,
885
886    /// Peers for which we are the both-create "owner" (kept our own session
887    /// group on the lexicographic tiebreak) and are still awaiting a
888    /// *group-aware* proof that the peer adopted our group. While a peer is in
889    /// this set, only `decrypt_success` may confirm the session — a plaintext
890    /// confirmation probe/ack is NOT group-aware (it only proves the peer holds
891    /// *some* session, possibly its own pre-adoption group) and must not be
892    /// allowed to stop our Welcome retransmission, or the peer could be left
893    /// stranded on a divergent group. Persisted (see
894    /// `restore_both_create_awaiting_decrypt`) so an owner restart mid-convergence
895    /// keeps the gate, rather than letting a stale plaintext probe/ack confirm
896    /// prematurely. Mutate only via `mark_both_create_awaiting_decrypt` /
897    /// `clear_both_create_awaiting_decrypt` to keep memory and storage in sync.
898    both_create_awaiting_decrypt: std::collections::HashSet<String>,
899
900    /// Sink for MLS lifecycle telemetry.
901    mls_event_emitter: Arc<dyn MlsEventEmitter>,
902
903    /// Rate limiting policy for MLS failure event floods.
904    mls_event_rate_limiter: MlsEventRateLimiter,
905
906    /// Pre-install scrubber used by MLS emit sites before a sink is
907    /// installed (`enable_telemetry`, or `install_sink` beneath it). Once one
908    /// is installed, emit sites read `self.telemetry.scrubber` instead via
909    /// `current_scrubber()`.
910    /// Both scrubbers share `telemetry_fallback_secret` so opaque identifiers
911    /// observed by the legacy `MlsEventEmitter` stay consistent across the
912    /// install boundary.
913    telemetry_scrubber: Scrubber,
914
915    /// Per-instance fallback secret for identifier scrubbing. Random at
916    /// construction. Reused by both the pre-install `telemetry_scrubber`
917    /// and by any scrubber built inside `install_sink` (unless the
918    /// installed `TelemetryConfig` carries its own `scrub_secret`). Keeping
919    /// the fallback stable across installs means the legacy
920    /// `MlsEventEmitter` path observes consistent opaque IDs before and
921    /// after a sink is installed on the same protocol instance.
922    telemetry_fallback_secret: [u8; 16],
923
924    /// Whether the persistent per-install scrub secret has been loaded from
925    /// (or initialized into) secure storage yet. Starts `false`; set `true`
926    /// the first time `restore_or_init_scrub_secret` succeeds so the load is
927    /// idempotent across the two storage-entry paths
928    /// (`initialize_mls` also enables persistence, which would otherwise
929    /// re-load and rebuild the scrubber a second time).
930    telemetry_secret_persisted: bool,
931
932    /// Whether the persistent per-install Nostr signing secret has been
933    /// loaded from (or initialized into) secure storage and installed into
934    /// the Nostr transport yet. Same idempotency role across the two
935    /// storage-entry paths as `telemetry_secret_persisted`; see
936    /// `restore_or_init_nostr_signing_secret`.
937    nostr_secret_persisted: bool,
938
939    /// A Nostr signing secret that was installed into the transport but
940    /// could not be persisted (storage `store` failed). Kept so the next
941    /// `restore_or_init_nostr_signing_secret` attempt retries persisting
942    /// this same secret instead of rotating the install's relay-visible
943    /// identity again mid-session.
944    nostr_unpersisted_secret: Option<Zeroizing<[u8; 32]>>,
945
946    /// Installed telemetry context (sink + config + scrubber). `None` until
947    /// `enable_telemetry` installs one; thereafter shared with
948    /// `SharedState` via `Arc` clone so both emit paths dispatch through the
949    /// same configuration.
950    ///
951    /// The duplication with [`SharedState::telemetry`] is deliberate: MLS
952    /// lifecycle emission is driven from `&self` on `OfflineProtocol` and
953    /// does not hold the shared-state lock, while protocol-event emission
954    /// happens inside `SharedState::emit_event` under the lock. Each path
955    /// reads the context from whichever side it already has in hand, and
956    /// `install_sink` is the single writer that keeps both copies in sync
957    /// (guaranteed atomic from the caller's perspective because `&mut self`
958    /// excludes concurrent calls), and `detach_telemetry_pipe` the single
959    /// clearer.
960    pub(crate) telemetry: Option<Arc<TelemetryContext>>,
961
962    /// The running telemetry pipe, when `enable_telemetry` built one. Its
963    /// sink is what `telemetry` dispatches into; the pipe itself is kept so
964    /// the tick can wake it and `initialize_mls` can hand it storage.
965    #[cfg(feature = "telemetry-pipe")]
966    telemetry_pipe: Option<Arc<TelemetryPipe>>,
967
968    /// File transfer manager for chunking outbound and reassembling inbound media.
969    file_transfer_manager: FileTransferManager,
970
971    /// Stashed (content_type, media_metadata) from chunk-0 of incoming file transfers,
972    /// used to populate the FileReceived event once all chunks arrive.
973    pending_media_metadata: HashMap<String, PendingMediaMetadataEntry>,
974    /// Tracks outbound media transfer delivery progress keyed by file_id.
975    outbound_media_transfers: HashMap<String, OutboundMediaTransfer>,
976    /// Maps each outbound media chunk message ID to (file_id, chunk_index).
977    outbound_media_chunks: HashMap<MessageId, (String, u32)>,
978    /// Sliding-window state for outbound file transfers, keyed by file_id.
979    /// Chunks are only sent when the window has capacity (previous chunks ACKed).
980    outbound_media_windows: HashMap<String, OutboundTransferState>,
981    /// Descriptors of transfers that were in flight when the previous process
982    /// died, restored from storage. `start()` emits one `MediaResendRequired`
983    /// event each but deliberately does NOT drain them: entries stay parked
984    /// (and persisted) until a same-`file_id` resend consumes them — after
985    /// `send_media_with` checksum-validates the re-supplied bytes against
986    /// the descriptor — or the restore TTL prunes them, so an app that
987    /// misses the signal gets it again next restart.
988    restored_media_descriptors: HashMap<String, MediaTransferDescriptor>,
989
990    /// Mesh service registry and handler (extracted crate).
991    mesh_services: MeshServices,
992
993    /// Bundled state for mesh group messaging (member cache, dedup, pending commits).
994    pub(crate) group_mesh: crate::group_mesh::GroupMeshState,
995
996    /// Set of blocked user IDs. Messages from blocked users are silently
997    /// dropped (no ACK, no event). Persisted via `ProtocolStateStorage`.
998    blocked_users: HashSet<String>,
999
1000    /// Timestamp of the last `kick_pending_session_reconciliation` execution.
1001    /// Used to throttle reconciliation to avoid expensive storage I/O
1002    /// (list_sessions → Keychain/Keystore) on every process tick / receive poll.
1003    last_reconciliation_at: Option<Instant>,
1004
1005    /// Instant of the last periodic `MetricsFrame` emission, used to rate
1006    /// the telemetry aggregator to `TelemetryConfig::metrics_cadence`.
1007    last_metrics_emit_at: Option<Instant>,
1008
1009    /// Last observed per-transport status map, used by the telemetry
1010    /// aggregator to diff against the current map and emit one
1011    /// `TransportStateEvent` per transition.
1012    transport_status_snapshot: HashMap<TransportType, TransportStatus>,
1013
1014    /// Last observed (battery, charging, relay_role) snapshot, used by the
1015    /// telemetry aggregator to fire `DeviceCapabilitySnapshot` on change.
1016    device_capability_snapshot: Option<DeviceSnap>,
1017
1018    /// The Lamport clock value at the time of the last storage write.
1019    /// Used to debounce `persist_lamport_clock()` — only write when the
1020    /// in-memory value has advanced past this by `LAMPORT_PERSIST_INTERVAL`
1021    /// ticks. On crash, at most `LAMPORT_PERSIST_INTERVAL` ticks are lost,
1022    /// which is harmless for a logical clock (the gap is absorbed by the
1023    /// next merge with any peer).
1024    last_persisted_lamport: u64,
1025
1026    /// Receive-side changes to the deduplicator's seen set since it was last
1027    /// written. Drives the batched write in `persist_dedup_seen_if_due`;
1028    /// see `DedupSeenRecord` for why the set is persisted at all.
1029    dedup_dirty: u32,
1030
1031    /// When the seen set was last written (or restored), for the time half of
1032    /// the batching rule. Starts at construction so the first write waits for
1033    /// either the interval or the dirty threshold like every later one.
1034    dedup_last_persist: Instant,
1035
1036    /// The Nostr receive watermark last written to storage, or `None` if this
1037    /// session has neither written nor restored one. Debounces
1038    /// `persist_nostr_watermark()` the same way `last_persisted_lamport`
1039    /// debounces the clock.
1040    ///
1041    /// Deliberately *not* captured for `initialize_mls` rollback, matching
1042    /// where the restore itself runs: the mark lives in the transport, which
1043    /// the rollback cannot reach, and re-installing a durable value that only
1044    /// ever moves forward is not something a failed MLS restore needs undone.
1045    last_persisted_nostr_watermark: Option<i64>,
1046
1047    /// Which MLS key package stands in each published Nostr slot.
1048    ///
1049    /// Persisted, because the alternative to remembering it is republishing
1050    /// under fresh slot ids on every launch — leaving the old records standing
1051    /// with packages we would no longer track for consumption.
1052    nostr_publication_slots: Vec<nostr_publication::NostrPublicationSlot>,
1053
1054    /// Slot ids published during *this* process.
1055    ///
1056    /// Deliberately not persisted: an addressable record lives on the relays,
1057    /// so a launch cannot know which of them still hold it. Publishing each
1058    /// slot once per process is the cheap way to be sure — it replaces rather
1059    /// than accumulates, so the cost of being wrong is one redundant event.
1060    nostr_published_slots: HashSet<String>,
1061
1062    /// When the publication slots were last re-scanned, throttling the MLS
1063    /// storage reads that scan costs. See `nostr_slot_refresh_due`.
1064    last_nostr_slot_refresh: Option<Instant>,
1065
1066    /// Per-slot republish backoff after publications that never reached a
1067    /// relay. Memory-only, and pruned to the live slot ids each refresh.
1068    nostr_publication_backoff: HashMap<String, nostr_publication::PublicationBackoff>,
1069
1070    /// When the slot-exhaustion security warning was last emitted, suppressing
1071    /// repeats for a cause that by nature persists.
1072    last_nostr_slot_warning: Option<Instant>,
1073
1074    /// The username this install has published a discovery claim for, as last
1075    /// persisted. `None` means no claim stands.
1076    ///
1077    /// Persisted, because it is the only thing that knows *which* name to
1078    /// retract when the profile changes or the feature is turned off. See
1079    /// `storage_keys::NOSTR_DISCOVERY_CLAIM`.
1080    nostr_discovery_claim: Option<Username>,
1081
1082    /// Names whose tombstone has not yet reached a relay, oldest first.
1083    ///
1084    /// Persisted beside the claim, and for a stronger reason than the claim
1085    /// is: the claim can be rebuilt from `config.profile`, while a retracted
1086    /// name has no other record anywhere once the profile has moved on.
1087    /// Dropping it before the tombstone lands strands a human-readable name in
1088    /// a public directory pointing at an address whose key packages are still
1089    /// being published, so nothing downstream expires it.
1090    nostr_discovery_retracting: Vec<Username>,
1091
1092    /// Whether the standing claim has been published during *this* process.
1093    ///
1094    /// Not persisted, for the same reason `nostr_published_slots` is not: an
1095    /// addressable record lives on the relays and a launch cannot know which
1096    /// of them still hold it. Republishing once per process replaces rather
1097    /// than accumulates.
1098    nostr_discovery_published: bool,
1099
1100    /// Republish backoff for the discovery claim, keyed by its tag.
1101    ///
1102    /// Memory-only and structurally identical to
1103    /// `nostr_publication_backoff`: a relay that rejects kind 30777 outright
1104    /// would otherwise be retried once per refresh forever.
1105    nostr_discovery_backoff: HashMap<String, nostr_publication::PublicationBackoff>,
1106
1107    /// When the discovery claim was last re-examined, throttling the tick.
1108    last_nostr_discovery_refresh: Option<Instant>,
1109
1110    /// In-flight username resolutions, keyed by query id.
1111    ///
1112    /// A resolution accumulates every verified claim for its username and
1113    /// emits **one** event carrying the whole set. See
1114    /// `nostr_discovery::PendingResolution` for why the set is the only thing
1115    /// that is ever emitted.
1116    nostr_resolutions: HashMap<String, nostr_discovery::PendingResolution>,
1117
1118    /// Username lookups requested but not yet minted into a relay query, with
1119    /// the time each was asked for.
1120    ///
1121    /// The transport holds the queue; this holds the *deadline*. They are
1122    /// separate because minting is what may never happen: the platform pumps
1123    /// queries only while the relay socket is up, so a lookup requested offline
1124    /// would otherwise have no clock running behind it and no event to arrive.
1125    /// Bounded by the transport's own resolve-queue ceiling, since an entry is
1126    /// recorded only when the transport accepts the lookup.
1127    nostr_resolution_requests: HashMap<Username, Instant>,
1128
1129    /// When the push key-package pool-exhaustion warning was last emitted.
1130    ///
1131    /// Suppressed for the same reason the slot warning is: the condition is a
1132    /// pool at its ceiling, which persists until packages are consumed or
1133    /// expire, and every push while it holds would otherwise emit.
1134    last_push_key_package_warning: Option<Instant>,
1135
1136    /// When the last `GatewayAddressDeclarationRefused` and
1137    /// `GatewayAddressBindingMismatch` warnings were emitted, for the same
1138    /// suppression the push-key-package warning uses.
1139    last_gateway_refusal_warning: Option<Instant>,
1140    last_gateway_binding_mismatch_warning: Option<Instant>,
1141}
1142
1143impl Drop for OfflineProtocol {
1144    fn drop(&mut self) {
1145        // Flush debounced Lamport clock so no ticks are lost when the
1146        // protocol is dropped without an explicit stop() call.
1147        self.flush_lamport_clock();
1148        self.flush_nostr_watermark();
1149        // And the deduplicator's seen set, whose write is batched the same
1150        // way: a copy of a message received in the last few seconds before
1151        // the drop would otherwise be re-processed by the next launch.
1152        self.flush_dedup_seen();
1153        // Same reason: edits batch before they reach a record, so without a
1154        // flush here the debounce window between an edit and its delta is a
1155        // window in which work is lost.
1156        #[cfg(feature = "data")]
1157        let _ = self.data_flush_all();
1158    }
1159}
1160
1161#[cfg(test)]
1162struct TestProtocolStateStorage {
1163    storage: Arc<dyn MlsStorage>,
1164}
1165
1166/// Maps the fixture's MLS-storage failures onto the protocol-state contract,
1167/// mirroring what the UniFFI adapter does for real providers.
1168#[cfg(test)]
1169pub(crate) fn map_test_storage_error(
1170    error: offline_protocol_mls::StorageError,
1171) -> crate::ProtocolStateError {
1172    use crate::ProtocolStateError as P;
1173    use offline_protocol_mls::StorageError as S;
1174    match error {
1175        S::KeyNotFound(detail) => P::NotFound(detail),
1176        S::CorruptedData(detail) => P::Corrupted(detail),
1177        S::StoreFailed(detail) => P::StoreFailed(detail),
1178        S::LoadFailed(detail) => P::LoadFailed(detail),
1179        S::DeleteFailed(detail) => P::DeleteFailed(detail),
1180        S::Unavailable(detail) => P::LoadFailed(detail),
1181        // Required by `#[non_exhaustive]` on StorageError. This fixture mirrors
1182        // the UniFFI adapter, whose own fallback also degrades to `LoadFailed`.
1183        ref other => P::LoadFailed(other.to_string()),
1184    }
1185}
1186
1187#[cfg(test)]
1188impl ProtocolStateStorage for TestProtocolStateStorage {
1189    fn store(&self, key_type: &str, key_id: &str, data: &[u8]) -> crate::ProtocolStateResult<()> {
1190        self.storage
1191            .store(key_type, key_id, data)
1192            .map_err(map_test_storage_error)
1193    }
1194
1195    fn load(&self, key_type: &str, key_id: &str) -> crate::ProtocolStateResult<Option<Vec<u8>>> {
1196        self.storage
1197            .load(key_type, key_id)
1198            .map_err(map_test_storage_error)
1199    }
1200
1201    fn delete(&self, key_type: &str, key_id: &str) -> crate::ProtocolStateResult<()> {
1202        self.storage
1203            .delete(key_type, key_id)
1204            .map_err(map_test_storage_error)
1205    }
1206
1207    fn list_keys(&self, key_type: &str) -> crate::ProtocolStateResult<Vec<String>> {
1208        self.storage
1209            .list_keys(key_type)
1210            .map_err(map_test_storage_error)
1211    }
1212}
1213
1214impl OfflineProtocol {
1215    /// Creates a new protocol instance.
1216    ///
1217    /// # Arguments
1218    ///
1219    /// * `config` - Protocol configuration
1220    ///
1221    /// # Returns
1222    ///
1223    /// Returns `Ok(OfflineProtocol)` if successful, `Err` if configuration is invalid.
1224    pub fn new(config: ProtocolConfig) -> Result<Self> {
1225        // Validate configuration
1226        config.validate()?;
1227
1228        // Create transport selector for DORS
1229        let transport_selector = TransportSelector::with_config(config.dors.clone());
1230
1231        // Create transport manager
1232        let transport_manager = TransportManager::new(transport_selector);
1233
1234        // Per-instance fallback secret for identifier scrubbing. Stable for
1235        // the life of this protocol instance so opaque IDs stay consistent
1236        // across any later `install_telemetry_sink` call that does not
1237        // supply its own `scrub_secret`.
1238        let telemetry_fallback_secret = *uuid::Uuid::new_v4().as_bytes();
1239
1240        Ok(Self {
1241            transport_manager,
1242            ack_manager: AckManager::with_config(config.reliability.ack.clone()),
1243            retry_queue: RetryQueue::with_config(config.reliability.retry.clone()),
1244            deduplicator: Deduplicator::with_config(config.reliability.dedup.clone()),
1245            mesh_relay: MeshRelayGovernor::with_config(
1246                config.profile.clone(),
1247                config.mesh_relay.clone(),
1248            ),
1249            custody: custody::CustodyStore::new(config.custody.clone()),
1250            custody_receipts: HashMap::new(),
1251            custody_last_sweep: Instant::now(),
1252            local_id: config.profile.clone(),
1253            identity_established: false,
1254            shared_state: Arc::new(Mutex::new(SharedState::new())),
1255            outbox: HashMap::new(),
1256            pending_reseal: HashMap::new(),
1257            staged_app_send: None,
1258            undecryptable_resent: HashSet::new(),
1259            media_outbox: HashMap::new(),
1260            window_held: HashMap::new(),
1261            dm_unreachable_parks: HashMap::new(),
1262            unrouted_acks: VecDeque::new(),
1263            reachability: reachability::ReachabilityFacts::default(),
1264            gateway_capabilities: HashSet::new(),
1265            mls_manager: None,
1266            pending_encrypted_messages: HashMap::new(),
1267            deferred_restore_settlements: Vec::new(),
1268            suppressed_restore_settlements: 0,
1269            next_pending_message_expiry: None,
1270            pending_key_packages: HashMap::new(),
1271            key_package_sent_to: HashMap::new(),
1272            known_peers: HashMap::new(),
1273            confirmed_sessions: std::collections::HashSet::new(),
1274            encryption_capable_peers: std::collections::HashSet::new(),
1275            peer_compact_envelope: std::collections::HashSet::new(),
1276            peer_ctrl_freshness: std::collections::HashSet::new(),
1277            peer_undecryptable_ack: std::collections::HashSet::new(),
1278            peer_coalesced_ack: std::collections::HashSet::new(),
1279            coalesced_acks_out: HashMap::new(),
1280            control_freshness_peers: std::collections::HashSet::new(),
1281            control_reset_watermark: std::collections::HashMap::new(),
1282            peer_rich_payload: std::collections::HashSet::new(),
1283            peer_svc_sealed: std::collections::HashSet::new(),
1284            svc_sealed_proved_peers: std::collections::HashSet::new(),
1285            peer_data_sync: std::collections::HashSet::new(),
1286            peer_data_group: std::collections::HashSet::new(),
1287            peer_data_group_attested: std::collections::HashSet::new(),
1288            peer_data_media: std::collections::HashSet::new(),
1289            peer_data_tombstones: std::collections::HashSet::new(),
1290            peer_data_interest: std::collections::HashSet::new(),
1291            peer_data_group_blob: std::collections::HashSet::new(),
1292            peer_data_group_blob_attested: std::collections::HashSet::new(),
1293            peer_data_custody: std::collections::HashSet::new(),
1294            peer_rich_attested: std::collections::HashSet::new(),
1295            plaintext_send_warned: std::collections::HashSet::new(),
1296            plaintext_receive_warned: std::collections::HashSet::new(),
1297            encryption_capable_persisted: HashSet::new(),
1298            control_gate_warned: HashMap::new(),
1299            pending_queue: PendingDecryptionQueue::default(),
1300            secure_storage: None,
1301            protocol_state_storage: None,
1302            state_record_cipher: None,
1303            #[cfg(feature = "data")]
1304            data: data::DataLayer::default(),
1305            #[cfg(feature = "data")]
1306            last_data_sync_offer: HashMap::new(),
1307            #[cfg(feature = "data")]
1308            data_sync_settle: HashMap::new(),
1309            #[cfg(feature = "data")]
1310            pending_attachment_fetches: HashMap::new(),
1311            blob_request_windows: HashMap::new(),
1312            #[cfg(feature = "data")]
1313            group_spaces_enumerated: false,
1314            lamport_clock: LamportClock::new(),
1315            confirmation_retry_due_at: HashMap::new(),
1316            confirmation_probe_due_at: HashMap::new(),
1317            confirmation_probe_unreachable_parks: HashMap::new(),
1318            confirmation_probe_outstanding: HashMap::new(),
1319            confirmation_probe_superseded: HashMap::new(),
1320            retry_drain_tokens: 0.0,
1321            retry_drain_last: None,
1322            rekey_due_at: HashMap::new(),
1323            corroboration_due_at: HashMap::new(),
1324            corroboration_challenges: HashMap::new(),
1325            session_build_stamps: HashMap::new(),
1326            corroboration_reask_at: HashMap::new(),
1327            welcome_lifecycles: HashMap::new(),
1328            pending_connection_requests: HashMap::new(),
1329            welcome_presence_rescue: HashMap::new(),
1330            both_create_awaiting_decrypt: std::collections::HashSet::new(),
1331            mls_event_emitter: Arc::new(NoopMlsEventEmitter),
1332            mls_event_rate_limiter: MlsEventRateLimiter::default(),
1333            // The pre-install scrubber uses `TelemetryConfig::default()` —
1334            // `scrub_ids=true`, no explicit `scrub_secret`. Identifier hashing
1335            // is therefore on from the moment the protocol is constructed,
1336            // matching today's always-scrub MLS observability semantics. When
1337            // `install_telemetry_sink` later supplies a config with
1338            // `scrub_ids(false)`, a new scrubber is derived from that config
1339            // but reuses `telemetry_fallback_secret` so opaque IDs remain
1340            // stable across the install boundary.
1341            telemetry_scrubber: Scrubber::from_config(
1342                &TelemetryConfig::default(),
1343                telemetry_fallback_secret,
1344            ),
1345            telemetry_fallback_secret,
1346            telemetry_secret_persisted: false,
1347            nostr_secret_persisted: false,
1348            nostr_unpersisted_secret: None,
1349            telemetry: None,
1350            #[cfg(feature = "telemetry-pipe")]
1351            telemetry_pipe: None,
1352            file_transfer_manager: FileTransferManager::new(),
1353            pending_media_metadata: HashMap::new(),
1354            outbound_media_transfers: HashMap::new(),
1355            outbound_media_chunks: HashMap::new(),
1356            outbound_media_windows: HashMap::new(),
1357            restored_media_descriptors: HashMap::new(),
1358            mesh_services: MeshServices::new(),
1359            group_mesh: crate::group_mesh::GroupMeshState::default(),
1360            blocked_users: HashSet::new(),
1361            last_reconciliation_at: None,
1362            last_metrics_emit_at: None,
1363            transport_status_snapshot: HashMap::new(),
1364            device_capability_snapshot: None,
1365            last_persisted_lamport: 0,
1366            dedup_dirty: 0,
1367            dedup_last_persist: Instant::now(),
1368            last_persisted_nostr_watermark: None,
1369            nostr_publication_slots: Vec::new(),
1370            nostr_published_slots: HashSet::new(),
1371            last_nostr_slot_refresh: None,
1372            nostr_publication_backoff: HashMap::new(),
1373            last_nostr_slot_warning: None,
1374            nostr_discovery_claim: None,
1375            nostr_discovery_retracting: Vec::new(),
1376            nostr_discovery_published: false,
1377            nostr_discovery_backoff: HashMap::new(),
1378            last_nostr_discovery_refresh: None,
1379            nostr_resolutions: HashMap::new(),
1380            nostr_resolution_requests: HashMap::new(),
1381            last_push_key_package_warning: None,
1382            last_gateway_refusal_warning: None,
1383            last_gateway_binding_mismatch_warning: None,
1384            config,
1385        })
1386    }
1387
1388    /// Initializes MLS encryption and protocol persistence.
1389    ///
1390    /// This must be called before encryption can be used. The storage
1391    /// `secure_storage` must be a platform-native credential store.
1392    /// `protocol_state_storage` must be scoped to the app container and is
1393    /// removed when that container is deleted.
1394    ///
1395    /// Ownership model:
1396    /// - `OfflineProtocol` is the single authoritative owner of `MlsManager`
1397    /// - initialization is idempotent per protocol instance
1398    /// - subsequent calls return without replacing the existing manager
1399    /// - manager publication is transactional: restore must succeed before
1400    ///   `mls_manager` becomes visible to callers
1401    ///
1402    /// The two storage domains are intentionally different trait objects. This
1403    /// prevents message-plane state from inheriting the lifecycle of Keychain
1404    /// or another credential store.
1405    pub fn initialize_mls(
1406        &mut self,
1407        secure_storage: Arc<dyn MlsStorage>,
1408        protocol_state_storage: Arc<dyn ProtocolStateStorage>,
1409    ) -> Result<()> {
1410        self.initialize_mls_inner(secure_storage, protocol_state_storage, true)
1411    }
1412
1413    /// Returns this device's address, once it has one.
1414    ///
1415    /// `None` before `initialize_mls`: the address is the hash of an identity
1416    /// key that lives in this profile's storage, and storage does not arrive
1417    /// until then. It never changes afterwards, and it is stable across
1418    /// restarts of the same profile because the key is.
1419    pub fn local_address(&self) -> Option<&str> {
1420        self.identity_established.then_some(self.local_id.as_str())
1421    }
1422
1423    /// `adopt_legacy_state` exists only so test fixtures can point both handles
1424    /// at one backend. Adoption moves records *between* the two stores, which
1425    /// on a shared backend would mean deleting a record through the same store
1426    /// it was just read from.
1427    ///
1428    /// There is one identity path and every caller takes it — a fixture that
1429    /// wants a particular identity seeds the key into `secure_storage` first
1430    /// (see [`crate::test_identity`]) rather than naming an id here, because an
1431    /// id nothing holds a key for is exactly what `MlsManager::new` refuses.
1432    fn initialize_mls_inner(
1433        &mut self,
1434        secure_storage: Arc<dyn MlsStorage>,
1435        protocol_state_storage: Arc<dyn ProtocolStateStorage>,
1436        adopt_legacy_state: bool,
1437    ) -> Result<()> {
1438        if self.mls_manager.is_some() {
1439            return Ok(());
1440        }
1441
1442        // The bootstrap inversion: load (or mint) the identity key first, and
1443        // take this device's wire identity from it. Before this point the
1444        // protocol has no address — `local_id` is still the profile — which is
1445        // exactly why the key has to be resolved here, ahead of the manager
1446        // that embeds the id in its credential and in every session slot.
1447        let (_, address) = MlsManager::load_or_create_identity(&secure_storage)?;
1448        let local_id = address.to_string();
1449
1450        let mut manager = MlsManager::new(&local_id, secure_storage.clone())?;
1451        // Installed before the manager becomes shared: the enforcement flag is
1452        // read on every group decrypt, and this is the one point where the
1453        // manager is still uniquely owned.
1454        manager.set_enforce_admin_commits(self.config.group.enforce_admin_commits);
1455        let manager = Arc::new(RwLock::new(manager));
1456
1457        // Keep initialization transactional so a restore failure cannot leave
1458        // partially-initialized MLS state visible and then permanently block retries.
1459        let previous_secure_storage = self.secure_storage.clone();
1460        let previous_protocol_state_storage = self.protocol_state_storage.clone();
1461        // The record cipher belongs to the secure store it was loaded from, so
1462        // it is part of the same transaction as the storage handles: taken here
1463        // so a failed init cannot leave the new store's key installed next to
1464        // the rolled-back handles, and restored below alongside them.
1465        let previous_state_record_cipher = self.state_record_cipher.take();
1466        let previous_pending_messages = self.pending_encrypted_messages.clone();
1467        let previous_pending_message_expiry = self.next_pending_message_expiry;
1468        // The inbound half of the same transaction: the restore re-admits
1469        // parked frames from the store being attached, so a rollback has to put
1470        // back what the queue held before, not an empty queue.
1471        let previous_pending_queue = self.pending_queue.clone();
1472        // Populated by restore steps that run before other fallible ones, so
1473        // they belong in the transaction like everything else: a failed init
1474        // must not leave a key-package cache, parked media descriptors, or an
1475        // owner gate sourced from a store the rollback has just detached.
1476        let previous_pending_key_packages = self.pending_key_packages.clone();
1477        let previous_restored_media_descriptors = self.restored_media_descriptors.clone();
1478        let previous_both_create_awaiting_decrypt = self.both_create_awaiting_decrypt.clone();
1479        let previous_confirmed_sessions = self.confirmed_sessions.clone();
1480        let previous_encryption_capable_peers = self.encryption_capable_peers.clone();
1481        let previous_welcome_lifecycles = self.welcome_lifecycles.clone();
1482        let previous_lamport_clock = self.lamport_clock.value();
1483        // The debounce watermark travels with the clock: `restore_lamport_clock`
1484        // sets both, so rolling back only the clock leaves a watermark ahead of
1485        // it and `persist_lamport_clock`'s `wrapping_sub` reads that as an
1486        // enormous delta — writing on every tick instead of every interval.
1487        let previous_last_persisted_lamport = self.last_persisted_lamport;
1488        let previous_blocked_users = self.blocked_users.clone();
1489        let previous_outbox = self.outbox.clone();
1490        let previous_peer_compact_envelope = self.peer_compact_envelope.clone();
1491        let previous_peer_ctrl_freshness = self.peer_ctrl_freshness.clone();
1492        let previous_peer_undecryptable_ack = self.peer_undecryptable_ack.clone();
1493        let previous_peer_coalesced_ack = self.peer_coalesced_ack.clone();
1494        // Snapshotted with its siblings so a failed re-initialize restores it
1495        // too. It is a ratchet, so the direction that matters is that a
1496        // rollback must not *lose* it: a peer we hold to the newer payload has
1497        // to still be held to it if the re-initialize is abandoned.
1498        let previous_control_freshness_peers = self.control_freshness_peers.clone();
1499        let previous_control_reset_watermark = self.control_reset_watermark.clone();
1500        let previous_peer_rich_payload = self.peer_rich_payload.clone();
1501        let previous_peer_svc_sealed = self.peer_svc_sealed.clone();
1502        let previous_svc_sealed_proved_peers = self.svc_sealed_proved_peers.clone();
1503        let previous_peer_data_sync = self.peer_data_sync.clone();
1504        let previous_peer_rich_attested = self.peer_rich_attested.clone();
1505        let previous_peer_data_group = self.peer_data_group.clone();
1506        let previous_peer_data_group_attested = self.peer_data_group_attested.clone();
1507        let previous_peer_data_media = self.peer_data_media.clone();
1508        let previous_peer_data_tombstones = self.peer_data_tombstones.clone();
1509        let previous_peer_data_interest = self.peer_data_interest.clone();
1510        let previous_peer_data_group_blob = self.peer_data_group_blob.clone();
1511        let previous_peer_data_group_blob_attested = self.peer_data_group_blob_attested.clone();
1512
1513        let previous_local_id = std::mem::replace(&mut self.local_id, local_id.clone());
1514        let previous_identity_established = self.identity_established;
1515        self.identity_established = true;
1516        // The governor took the profile at construction because no address
1517        // existed yet; it keys relay fan-out and jitter off the local id, so it
1518        // has to move to the real one here.
1519        self.mesh_relay.set_local_id(local_id);
1520
1521        self.secure_storage = Some(secure_storage);
1522        self.protocol_state_storage = Some(protocol_state_storage);
1523
1524        // Must precede every restore below: sealed protocol-state records
1525        // (pending messages, outbox, media descriptors) are unreadable without
1526        // this key, so restoring first would silently start from empty and then
1527        // overwrite durable state with that empty view.
1528        self.restore_or_init_state_record_key();
1529
1530        // Then adopt anything the pre-split build left in secure storage, so
1531        // the restores below see one complete view. Must follow the record key
1532        // (adoption seals what it moves) and precede every restore (a restore
1533        // that ran first would find nothing and then overwrite the legacy state
1534        // with that empty view). Best-effort and resumable: a failure here
1535        // leaves the legacy records in place for the next launch.
1536        if adopt_legacy_state {
1537            self.adopt_legacy_protocol_state();
1538        }
1539
1540        // The settlement queue is deliberately NOT captured for rollback, and
1541        // that is the whole point rather than an omission.
1542        //
1543        // The rollback below restores *in-memory* state. It restores no storage
1544        // state, because restore has none to give back: by the time a later
1545        // step fails, an earlier one has already deleted an unaddressable
1546        // pending queue, dropped a record its store reported corrupt, and
1547        // rewritten a peer's snapshot without the entries it evicted for
1548        // capacity. Those records are gone. So the invariant adoption is
1549        // documented with — nothing can re-derive a settlement for a record
1550        // that no longer exists — is not special to adoption; it holds for
1551        // every settlement produced under this transaction, and rolling any of
1552        // them back means the application keeps an id that resolves to nothing,
1553        // on this launch and on every launch after it.
1554        //
1555        // The cost of not rolling back is that a retry which re-examines a
1556        // still-present record can settle the same id twice. `message_failed`
1557        // is terminal and idempotent for any reasonable consumer, and a
1558        // duplicate terminal event is a far smaller lie than silence.
1559        //
1560        // Load the persistent scrub secret outside the transactional restore
1561        // below: it is independent of MLS state, and a later MLS-restore
1562        // rollback must not undo it (the secret is idempotent and reused on
1563        // the next attempt).
1564        self.restore_or_init_scrub_secret();
1565
1566        // Same reasoning for the Nostr signing secret: independent of MLS
1567        // state, idempotent, and must survive an MLS-restore rollback.
1568        self.restore_or_init_nostr_signing_secret();
1569
1570        // And for the Nostr receive watermark, which sits outside the
1571        // transaction below for a third reason on top of those two: it does
1572        // not live in `self` at all. The mark is transport state, so the
1573        // rollback — which restores in-memory fields of `OfflineProtocol` —
1574        // could not put it back even if it wanted to. Installing it under a
1575        // later failure is harmless: it only ever moves forward, and the worst
1576        // a stale-but-installed mark does is narrow one replay window.
1577        self.restore_nostr_watermark();
1578        self.restore_nostr_publication_slots();
1579        self.restore_nostr_discovery_claim();
1580
1581        // The launch's whole durable-delete allowance, in one place because the
1582        // bound is on the launch. A device-barrier storm kills *this call*, not
1583        // any single walk in it, so no walk may hand itself a pool — see
1584        // `PruneAllowance::pool`. Four of them, and the ceiling is their sum:
1585        //
1586        // - `advisory_prunes` is shared by the `ADVISORY_PRUNE_WALKS` walks
1587        //   whose prunes are caches or advisory signals. They draw in the order
1588        //   written below, and that order is part of the contract: each leaves
1589        //   the ones after it a `MIN_ADVISORY_PRUNE_DELETES` floor and may take
1590        //   the rest, so an earlier walk gets first call on whatever the later
1591        //   ones do not reserve. Reordering them, or adding a sixth without
1592        //   bumping `ADVISORY_PRUNE_WALKS`, changes who gets pruned under load.
1593        // - `pending_prunes` and `outbox_prunes` are private to the two
1594        //   settlement-paired walks, because being starved there defers a
1595        //   diagnostic or a *delivery* rather than a cache eviction, and
1596        //   neither may be held hostage to a key-package flood in a category it
1597        //   has nothing to do with.
1598        // - `inbound_prunes` is private to the two receive-side walks for the
1599        //   same reason: the pending-decryption walk holds ciphertext the app
1600        //   is told about when it is lost. It draws first, and the seen-set
1601        //   restore takes whatever is left for its one possible delete.
1602        //
1603        // See `storage::MAX_RESTORE_PRUNE_DELETES`.
1604        let mut advisory_prunes = PruneAllowance::pool();
1605        let mut pending_prunes = PruneAllowance::pool();
1606        let mut outbox_prunes = PruneAllowance::pool();
1607        let mut inbound_prunes = PruneAllowance::pool();
1608
1609        // Restore state from previous session
1610        let restore_result = (|| {
1611            self.restore_pending_messages(&mut pending_prunes)?;
1612            self.restore_pending_decrypt_entries(&mut inbound_prunes);
1613            self.restore_custody(&mut inbound_prunes);
1614            self.restore_lamport_clock();
1615            self.restore_dedup_seen(&mut inbound_prunes);
1616            self.restore_encryption_capable_peers();
1617            self.restore_blocked_users()?;
1618            self.restore_session_states_from_manager(manager.clone(), &mut advisory_prunes)?;
1619            self.restore_peer_key_packages(&manager, &mut advisory_prunes)?;
1620            // Must precede start(): flush_restored_confirmed_pending_messages
1621            // re-makes the rich seal decision against these sets, and an
1622            // empty set there silently drops queued rich extras.
1623            self.restore_peer_capabilities(&manager, &mut advisory_prunes);
1624            self.restore_welcome_lifecycles(&mut advisory_prunes)?;
1625            self.restore_outbox(&mut outbox_prunes)?;
1626            self.restore_media_descriptors(&mut advisory_prunes)?;
1627            self.restore_both_create_awaiting_decrypt();
1628            Ok(())
1629        })();
1630
1631        if let Err(err) = restore_result {
1632            self.mesh_relay.set_local_id(previous_local_id.clone());
1633            self.local_id = previous_local_id;
1634            self.identity_established = previous_identity_established;
1635            self.secure_storage = previous_secure_storage;
1636            self.protocol_state_storage = previous_protocol_state_storage;
1637            self.state_record_cipher = previous_state_record_cipher;
1638            self.pending_encrypted_messages = previous_pending_messages;
1639            self.next_pending_message_expiry = previous_pending_message_expiry;
1640            // Back to what the queue held before this call. Emptying it lost
1641            // frames that were never persisted (no store was attached, or the
1642            // best-effort write failed). Keeping what the restore added would
1643            // leave entries sourced from the store the rollback just detached:
1644            // draining one deletes nothing on disk, so the next launch restores
1645            // it and drains it again into a spent ratchet generation.
1646            self.pending_queue = previous_pending_queue;
1647            // `deferred_restore_settlements` is deliberately left alone — see
1648            // the comment where the other baselines are captured.
1649            self.pending_key_packages = previous_pending_key_packages;
1650            self.restored_media_descriptors = previous_restored_media_descriptors;
1651            self.both_create_awaiting_decrypt = previous_both_create_awaiting_decrypt;
1652            self.confirmed_sessions = previous_confirmed_sessions;
1653            // Restored for consistency with the rest of the transaction, not
1654            // for safety: this set only ever grows, so keeping the wider view
1655            // would also have been sound. Rolling it back keeps "a failed
1656            // initialize_mls leaves no trace" true without exception.
1657            self.encryption_capable_peers = previous_encryption_capable_peers;
1658            self.welcome_lifecycles = previous_welcome_lifecycles;
1659            self.lamport_clock = LamportClock::from_value(previous_lamport_clock);
1660            self.last_persisted_lamport = previous_last_persisted_lamport;
1661            self.blocked_users = previous_blocked_users;
1662            self.outbox = previous_outbox;
1663            self.peer_compact_envelope = previous_peer_compact_envelope;
1664            self.peer_ctrl_freshness = previous_peer_ctrl_freshness;
1665            self.peer_undecryptable_ack = previous_peer_undecryptable_ack;
1666            self.peer_coalesced_ack = previous_peer_coalesced_ack;
1667            self.control_freshness_peers = previous_control_freshness_peers;
1668            self.control_reset_watermark = previous_control_reset_watermark;
1669            self.peer_rich_payload = previous_peer_rich_payload;
1670            self.peer_svc_sealed = previous_peer_svc_sealed;
1671            self.svc_sealed_proved_peers = previous_svc_sealed_proved_peers;
1672            self.peer_data_sync = previous_peer_data_sync;
1673            self.peer_rich_attested = previous_peer_rich_attested;
1674            self.peer_data_group = previous_peer_data_group;
1675            self.peer_data_group_attested = previous_peer_data_group_attested;
1676            self.peer_data_media = previous_peer_data_media;
1677            self.peer_data_tombstones = previous_peer_data_tombstones;
1678            self.peer_data_interest = previous_peer_data_interest;
1679            self.peer_data_group_blob = previous_peer_data_group_blob;
1680            self.peer_data_group_blob_attested = previous_peer_data_group_blob_attested;
1681            return Err(err);
1682        }
1683
1684        self.mls_manager = Some(manager);
1685        #[cfg(feature = "telemetry-pipe")]
1686        self.attach_telemetry_storage();
1687        // A re-init can swap the identity, and with it which groups exist, so
1688        // the one-shot group enumeration has to run again. Past the last
1689        // `return Err` above, so a failed init leaves the flag alone.
1690        #[cfg(feature = "data")]
1691        {
1692            self.group_spaces_enumerated = false;
1693        }
1694        self.emit_mls_initialized();
1695
1696        // Announced only here, past the last `return Err` — a restore failure
1697        // rolls the address back, and announcing an identity the instance then
1698        // abandoned would leave the app addressing a device that is not this
1699        // one.
1700        self.emit_event(Event::identity_ready(self.local_id.clone()));
1701
1702        info!(profile = %self.config.profile, "MLS encryption initialized with split secure and protocol-state storage");
1703        Ok(())
1704    }
1705
1706    /// Test-only adapter for fixtures that intentionally use one
1707    /// in-memory/fault-injection backend to observe both storage domains.
1708    #[cfg(test)]
1709    pub(crate) fn initialize_mls_for_test(&mut self, storage: Arc<dyn MlsStorage>) -> Result<()> {
1710        let protocol_state_storage = Arc::new(TestProtocolStateStorage {
1711            storage: storage.clone(),
1712        });
1713        // Give the storage the identity this fixture's profile label stands
1714        // for, then take the production path over it. The instance comes up as
1715        // a genuine derived address, and it is the *same* address every time a
1716        // fixture says `"alice"` — which is what lets tests keep readable
1717        // labels while still exercising derived identity.
1718        crate::test_identity::seed_identity(&storage, &self.config.profile);
1719        self.initialize_mls_inner(storage, protocol_state_storage, false)
1720    }
1721
1722    /// Test-only init that takes the **production** identity path: the address
1723    /// is derived from the identity key in `storage` rather than forced.
1724    #[cfg(test)]
1725    pub(crate) fn initialize_mls_for_test_derived(
1726        &mut self,
1727        storage: Arc<dyn MlsStorage>,
1728    ) -> Result<()> {
1729        let protocol_state_storage = Arc::new(TestProtocolStateStorage {
1730            storage: storage.clone(),
1731        });
1732        self.initialize_mls_inner(storage, protocol_state_storage, false)
1733    }
1734
1735    /// Test-only storage initialization for persistence-focused fixtures that
1736    /// do not need an MLS manager.
1737    #[cfg(test)]
1738    pub(crate) fn enable_message_persistence_for_test(
1739        &mut self,
1740        storage: Arc<dyn MlsStorage>,
1741    ) -> Result<()> {
1742        self.secure_storage = Some(storage.clone());
1743        self.protocol_state_storage = Some(Arc::new(TestProtocolStateStorage { storage }));
1744        self.restore_or_init_state_record_key();
1745        self.restore_or_init_scrub_secret();
1746        self.restore_or_init_nostr_signing_secret();
1747        self.restore_nostr_watermark();
1748        self.restore_nostr_publication_slots();
1749        self.restore_nostr_discovery_claim();
1750        // Same four pools as `initialize_mls_inner`, for the same reason.
1751        let mut advisory_prunes = PruneAllowance::pool();
1752        let mut pending_prunes = PruneAllowance::pool();
1753        let mut outbox_prunes = PruneAllowance::pool();
1754        let mut inbound_prunes = PruneAllowance::pool();
1755        self.restore_pending_messages(&mut pending_prunes)?;
1756        self.restore_pending_decrypt_entries(&mut inbound_prunes);
1757        self.restore_custody(&mut inbound_prunes);
1758        self.restore_lamport_clock();
1759        self.restore_dedup_seen(&mut inbound_prunes);
1760        self.restore_encryption_capable_peers();
1761        self.restore_blocked_users()?;
1762        self.restore_outbox(&mut outbox_prunes)?;
1763        self.restore_media_descriptors(&mut advisory_prunes)?;
1764        Ok(())
1765    }
1766
1767    /// Checks if MLS encryption is initialized.
1768    pub fn is_mls_initialized(&self) -> bool {
1769        self.mls_manager.is_some()
1770    }
1771
1772    /// Records that `peer_id` has demonstrated it runs MLS.
1773    ///
1774    /// The only way into [`Self::encryption_capable_peers`]. Call it from
1775    /// anywhere a peer proves encryption capability — creating or joining a 1:1
1776    /// session with them, or pinning their signing key. Calling it redundantly
1777    /// is free and calling it too often is safe; the failure that matters is
1778    /// *not* calling it, which leaves the plaintext gate open for that peer.
1779    ///
1780    /// # Bounded by refusal, never by eviction
1781    ///
1782    /// The keys are wire-claimed ids and need their own cap, like every other
1783    /// map keyed that way — but not for the reason this comment used to give.
1784    /// It cited an unsigned `__MLS_WELCOME__` reaching the marking path; the
1785    /// control gate now rejects unsigned control frames before dispatch, and
1786    /// `__MLS_WELCOME__` is in neither exemption list, so `handle_welcome_message`
1787    /// only ever runs on a verified frame.
1788    ///
1789    /// The cap survives that because a signature was never the bound. A
1790    /// signature proves the signer holds the key its address derives from; it
1791    /// does not prove the address belongs to anyone real, and minting a fresh
1792    /// one costs a keygen. So an attacker willing to spend keygens can still
1793    /// present an unbounded supply of distinct, perfectly well-signed peers —
1794    /// which is precisely what a cap on a wire-keyed map is for.
1795    ///
1796    /// It has to be a refusal. Evicting would un-mark a peer that really is
1797    /// encryption-capable — the fail-open direction this field exists to
1798    /// prevent — and would hand an attacker a way to unprotect a *chosen*
1799    /// victim by flooding. Refusing only declines to add knowledge: that peer
1800    /// falls through to the `is_session_confirmed` check in
1801    /// [`Self::accept_plaintext_content`], which is exactly what shipped before
1802    /// this set existed. So a flood costs later peers the improvement and never
1803    /// costs anyone the protection they already had.
1804    ///
1805    /// Legitimate peers are also first in line by construction: restore seeds
1806    /// the set from the session list and the durable capability records during
1807    /// `initialize_mls`, before `start()` admits any traffic.
1808    ///
1809    /// # Return value
1810    ///
1811    /// `true` when `peer_id` is in the set on return — whether this call added
1812    /// it or a previous one did. This is what bounds the *durable* record:
1813    /// [`Self::record_encryption_capable`](crate::OfflineProtocol) persists only
1814    /// when this returns `true`, so the storage category can never hold more
1815    /// peers than the cap below. Without that coupling a Sybil flood — cheap,
1816    /// since each identity is one Ed25519 keygen that signs honestly as itself
1817    /// — would write unbounded records, and the restore walk (bounded by
1818    /// `MAX_RESTORE_KEYS_PER_CATEGORY`) could then miss a legitimate peer's
1819    /// record and re-open the plaintext gate for them on the next launch.
1820    pub(crate) fn mark_encryption_capable(&mut self, peer_id: &str) -> bool {
1821        if peer_id.is_empty() || peer_id == self.local_id {
1822            return false;
1823        }
1824        if self.encryption_capable_peers.contains(peer_id) {
1825            return true;
1826        }
1827        if self.encryption_capable_peers.len() >= MAX_ENCRYPTION_CAPABLE_PEERS {
1828            debug!(
1829                peer_id = %peer_id,
1830                cap = MAX_ENCRYPTION_CAPABLE_PEERS,
1831                "Encryption-capability set at capacity; peer falls back to the session-state check"
1832            );
1833            return false;
1834        }
1835        self.encryption_capable_peers.insert(peer_id.to_string());
1836        debug!(peer_id = %peer_id, "Peer marked encryption-capable");
1837        if self.encryption_capable_peers.len() == MAX_ENCRYPTION_CAPABLE_PEERS {
1838            // Emitted on the transition into full, so it cannot become a
1839            // per-frame flood: the set only grows and has no removal path, so
1840            // this line is reachable exactly once per process.
1841            warn!(
1842                cap = MAX_ENCRYPTION_CAPABLE_PEERS,
1843                "Encryption-capability set reached capacity; further peers fall back to the \
1844                 session-state check. Expected only under a forged-sender flood."
1845            );
1846        }
1847        true
1848    }
1849
1850    /// Whether `peer_id` is known to run MLS. See
1851    /// [`Self::encryption_capable_peers`] for why this is not a storage read.
1852    pub(crate) fn is_encryption_capable(&self, peer_id: &str) -> bool {
1853        self.encryption_capable_peers.contains(peer_id)
1854    }
1855
1856    /// Drops `peer_id` from the in-memory capability set **only**, leaving its
1857    /// durable record and write-elision cache entry in place.
1858    ///
1859    /// Test-only, and deliberately not a general "forget" — the set has no
1860    /// removal path by design (see [`Self::encryption_capable_peers`]). It
1861    /// reproduces one specific state that production does reach: the rollback in
1862    /// `initialize_mls` restores the pre-restore snapshot of the set while
1863    /// [`Self::encryption_capable_persisted`] keeps what the restore walk read,
1864    /// so the two disagree until the peer's next verified frame re-marks them.
1865    #[cfg(test)]
1866    pub(crate) fn forget_encryption_capable_for_test(&mut self, peer_id: &str) {
1867        self.encryption_capable_peers.remove(peer_id);
1868    }
1869
1870    /// Returns whether auto-encryption should be applied.
1871    fn should_auto_encrypt(&self) -> bool {
1872        self.config.encryption.enabled && self.mls_manager.is_some()
1873    }
1874
1875    /// Configures the MLS lifecycle event emitter.
1876    pub fn set_mls_event_emitter(&mut self, emitter: Arc<dyn MlsEventEmitter>) {
1877        self.mls_event_emitter = emitter;
1878    }
1879
1880    /// Installs the unified telemetry sink for this protocol instance.
1881    ///
1882    /// Every subsequent protocol [`Event`] and MLS lifecycle event is
1883    /// additionally forwarded to `sink` as a
1884    /// [`crate::telemetry::TelemetryRecord`], gated by the verbosity tier
1885    /// and identifier-scrubbing preferences in `config`. Protocol events
1886    /// flow through as [`TelemetryRecord::Protocol`], MLS lifecycle events
1887    /// as [`TelemetryRecord::Mls`].
1888    ///
1889    /// This does not replace the legacy [`crate::EventCallback`] and
1890    /// [`MlsEventEmitter`] paths — they continue to fire independently for
1891    /// backward compatibility.
1892    ///
1893    /// Events emitted before this call (for example
1894    /// [`crate::mls_observability::MlsLifecycleEvent::Initialized`] fired
1895    /// during `initialize_mls`) are not replayed to a sink installed later.
1896    ///
1897    /// # Re-install semantics
1898    ///
1899    /// Calling this method a second time *replaces* the previously installed
1900    /// sink and config — the old sink stops receiving records as soon as
1901    /// this call returns. The per-instance fallback secret is preserved so
1902    /// opaque identifiers stay stable across the swap when the new config
1903    /// does not carry its own `scrub_secret`.
1904    ///
1905    /// The per-tick diff snapshots (transport status, device capability,
1906    /// metrics cadence) are **rearmed** on every install: the next
1907    /// `process()` tick observes the current state as its baseline and emits
1908    /// no synthetic transitions for transports that were already available
1909    /// at install time. Apps that need the current transport statuses at
1910    /// install time should pull them explicitly via
1911    /// [`TransportManager::get_all_transport_statuses`].
1912    ///
1913    /// The escalation-trigger dedupe window (`ESCALATION_TRIGGER_DEDUPE_SECS`
1914    /// inside [`TransportManager`]) is **not** rearmed. A sink installed
1915    /// within that window after an escalation event fired to a previous
1916    /// sink may miss the next occurrence of the same reason until the
1917    /// window elapses. This is intentional: the dedupe is a property of
1918    /// the routing engine, not of the observer.
1919    ///
1920    /// # Lifecycle
1921    ///
1922    /// Installing a sink wires the structured routing-decision callback
1923    /// on the underlying [`TransportManager`]. The callback lives for the
1924    /// life of the protocol instance or until another
1925    /// `install_telemetry_sink` call replaces it — `start()` and `stop()`
1926    /// do not toggle it. This means apps can install a sink before
1927    /// `start()` and see routing records from the very first `send()`,
1928    /// and a `stop() → start()` cycle preserves the wiring so no
1929    /// re-install is needed. Conversely, apps that never install a sink
1930    /// pay no per-`send()` routing-emission overhead.
1931    ///
1932    /// # Errors
1933    ///
1934    /// Returns `Err` if the shared-state mutex is poisoned (indicating an
1935    /// earlier panic in another thread while holding the lock). On error no
1936    /// sink is installed, and the legacy emission paths are unaffected.
1937    ///
1938    /// [`TelemetryRecord::Protocol`]: crate::telemetry::TelemetryRecord::Protocol
1939    /// [`TelemetryRecord::Mls`]: crate::telemetry::TelemetryRecord::Mls
1940    // Crate-private since 0.26: the pipe is the one production sink, and the
1941    // engine's own tests are the remaining callers.
1942    #[cfg_attr(not(test), allow(dead_code))]
1943    pub(crate) fn install_telemetry_sink(
1944        &mut self,
1945        sink: Arc<dyn TelemetrySink>,
1946        config: TelemetryConfig,
1947    ) -> Result<()> {
1948        self.install_sink(
1949            sink,
1950            config,
1951            Arc::new(std::sync::atomic::AtomicBool::new(true)),
1952        )
1953    }
1954
1955    /// Installs `sink` behind `gate`, the flag every emit site reads first.
1956    fn install_sink(
1957        &mut self,
1958        sink: Arc<dyn TelemetrySink>,
1959        config: TelemetryConfig,
1960        gate: Arc<std::sync::atomic::AtomicBool>,
1961    ) -> Result<()> {
1962        // Forward the routing-diagnostic preference to the TransportManager
1963        // before wiring any callbacks so the very first routing decision
1964        // post-install already reflects the requested tier.
1965        self.transport_manager
1966            .set_routing_diagnostic(config.routing_diagnostic());
1967
1968        let ctx = TelemetryContext::with_gate(sink, config, self.telemetry_fallback_secret, gate);
1969        let mut state = lock_shared_state(&self.shared_state).map_err(|err| {
1970            error!(
1971                error = %err,
1972                "install_telemetry_sink: shared-state mutex poisoned; sink NOT installed",
1973            );
1974            err
1975        })?;
1976        state.telemetry = Some(ctx.clone());
1977        drop(state);
1978        self.telemetry = Some(ctx);
1979
1980        // Wire the structured routing-decision callback here so the
1981        // `TransportManager` pays the per-`send()` emission cost only when
1982        // a sink is actually installed, and so the wiring persists across
1983        // `stop() → start()` cycles (the callback reads `s.telemetry` on
1984        // every invocation and that field stays set). A subsequent
1985        // `install_telemetry_sink` replaces the closure with one capturing
1986        // the fresh `shared_routing` clone; both closures read the same
1987        // `SharedState::telemetry` slot, so even a concurrent in-flight
1988        // emission from the pre-replace closure would still resolve to
1989        // the currently-installed sink.
1990        let shared_routing = self.shared_state.clone();
1991        self.transport_manager
1992            .set_routing_decision_callback(Some(Arc::new(move |decision| {
1993                let s = shared_routing.lock_or_recover();
1994                if let Some(ctx) = s.telemetry.as_ref().filter(|ctx| ctx.enabled()) {
1995                    let record = TelemetryRecord::Routing(Box::new(decision));
1996                    // Dispatch is panic-isolated so a sink that panics
1997                    // here cannot unwind through the live `MutexGuard`
1998                    // and poison `SharedState`.
1999                    dispatch_record(&ctx.sink, &record);
2000                }
2001            })));
2002
2003        // Rearm the per-tick diff snapshots so the first tick after install
2004        // reports honest transitions only. Without this, a sink installed
2005        // after start() would observe a synthetic `Unavailable → Available`
2006        // transition for every already-running transport on the next tick.
2007        let (statuses, available) = self.transport_manager.snapshot_status_and_available();
2008        self.transport_status_snapshot = statuses;
2009        let (battery_level, is_charging) =
2010            device_battery_from_available(self.transport_manager.current_transport(), &available);
2011        let relay_role = self.relay_role();
2012        self.device_capability_snapshot = Some(DeviceSnap::from_parts(
2013            battery_level,
2014            is_charging,
2015            relay_role,
2016        ));
2017        // Leave `last_metrics_emit_at` at None so the first tick after
2018        // install fires a fresh metrics snapshot — that is the bootstrap
2019        // payload a new sink actually wants (full counter state in one
2020        // record).
2021        self.last_metrics_emit_at = None;
2022
2023        Ok(())
2024    }
2025
2026    /// Enables telemetry: the SDK collects, batches and uploads accepted
2027    /// events to the ingest itself.
2028    ///
2029    /// `config` must carry the `api_key` and `app_id` the developer portal
2030    /// issued; [`TelemetryConfig::validate`] names any field that is refused,
2031    /// and the refusal is [`Error::TelemetryConfigInvalid`]. `host` names the
2032    /// platform stamped on every batch and the application state right now.
2033    ///
2034    /// Nothing waits on I/O here: the pipe's storage load and first upload
2035    /// run on its own thread. Batches are durable across a restart once a
2036    /// protocol-state store is attached, which on the mobile bindings
2037    /// happens inside `initialize_mls`; before that they are held in memory.
2038    /// Calling this again replaces the running pipe after its final flush.
2039    ///
2040    /// What leaves the device, when, and how to switch it off are in
2041    /// `docs/telemetry.md`.
2042    #[cfg(feature = "telemetry-pipe")]
2043    pub fn enable_telemetry(&mut self, config: TelemetryConfig, host: TelemetryHost) -> Result<()> {
2044        if let Err(field) = config.validate() {
2045            return Err(Error::TelemetryConfigInvalid(field.to_string()));
2046        }
2047        if let Some(previous) = self.detach_telemetry_pipe() {
2048            previous.stop(FINAL_FLUSH_BUDGET);
2049        }
2050        // Always started in memory, even when a store is already attached.
2051        // Adopting a backend loads the durable queue: an index read, up to
2052        // `MAX_PENDING_BATCHES` sealed record reads with a decrypt and a parse
2053        // each, a `list_keys` and the orphan sweep. Doing that here would run
2054        // it on the caller, holding `&mut self`, which on the mobile bindings
2055        // is the common order: `initialize_mls` has already attached storage
2056        // by the time an app enables telemetry. `attach_telemetry_storage`
2057        // hands the backend over instead, and the pipe's own thread adopts it.
2058        let device_id = self.telemetry_install_id();
2059        let pipe = TelemetryPipe::start(
2060            &config,
2061            PipeParts::production(host, Backend::Memory, device_id),
2062        )?;
2063        self.install_sink(pipe.sink(), config, pipe.enabled_flag())?;
2064        self.telemetry_pipe = Some(pipe);
2065        self.attach_telemetry_storage();
2066        Ok(())
2067    }
2068
2069    /// Disables telemetry: one final flush within [`FINAL_FLUSH_BUDGET`], then
2070    /// the uploader thread stops and every emit site goes back to its
2071    /// telemetry-off cost. Callers holding an outer lock should prefer
2072    /// [`Self::detach_telemetry_pipe`] and stop the pipe outside it.
2073    #[cfg(feature = "telemetry-pipe")]
2074    pub fn disable_telemetry(&mut self) {
2075        if let Some(pipe) = self.detach_telemetry_pipe() {
2076            pipe.stop(FINAL_FLUSH_BUDGET);
2077        }
2078    }
2079
2080    /// Detaches the running pipe without stopping it: the sink is
2081    /// uninstalled, the routing callback cleared, and the pipe handed back
2082    /// for the caller to stop once it holds no engine lock. Its final flush
2083    /// may block for up to [`FINAL_FLUSH_BUDGET`], which is why the two steps
2084    /// are separable.
2085    #[cfg(feature = "telemetry-pipe")]
2086    pub fn detach_telemetry_pipe(&mut self) -> Option<Arc<TelemetryPipe>> {
2087        let pipe = self.telemetry_pipe.take()?;
2088        self.telemetry = None;
2089        self.shared_state.lock_or_recover().telemetry = None;
2090        self.transport_manager.set_routing_decision_callback(None);
2091        self.transport_manager.set_routing_diagnostic(false);
2092        Some(pipe)
2093    }
2094
2095    /// The running telemetry pipe, if any.
2096    #[cfg(feature = "telemetry-pipe")]
2097    pub fn telemetry_pipe(&self) -> Option<Arc<TelemetryPipe>> {
2098        self.telemetry_pipe.clone()
2099    }
2100
2101    /// Stops or resumes collection without tearing the pipe down. Off, every
2102    /// emit site costs one atomic load; what is already queued still drains.
2103    #[cfg(feature = "telemetry-pipe")]
2104    pub fn set_telemetry_enabled(&self, enabled: bool) {
2105        if let Some(pipe) = &self.telemetry_pipe {
2106            pipe.set_enabled(enabled);
2107        }
2108    }
2109
2110    /// Asks the uploader to flush now. Returns immediately.
2111    #[cfg(feature = "telemetry-pipe")]
2112    pub fn flush_telemetry(&self) {
2113        if let Some(pipe) = &self.telemetry_pipe {
2114            pipe.flush();
2115        }
2116    }
2117
2118    /// Flushes and waits up to `budget` for the uploader to finish.
2119    #[cfg(feature = "telemetry-pipe")]
2120    pub fn flush_telemetry_blocking(&self, budget: std::time::Duration) -> bool {
2121        self.telemetry_pipe
2122            .as_ref()
2123            .is_some_and(|pipe| pipe.flush_blocking(budget))
2124    }
2125
2126    /// The pipe's counters, or `None` while telemetry is not enabled.
2127    #[cfg(feature = "telemetry-pipe")]
2128    pub fn telemetry_stats(&self) -> Option<TelemetryStats> {
2129        self.telemetry_pipe.as_ref().map(|pipe| pipe.stats())
2130    }
2131
2132    /// Closes the current telemetry session: summary, a blocking flush,
2133    /// then a fresh session id.
2134    #[cfg(feature = "telemetry-pipe")]
2135    pub fn end_telemetry_session(&self) {
2136        if let Some(pipe) = &self.telemetry_pipe {
2137            pipe.end_session();
2138        }
2139    }
2140
2141    /// Reports an application lifecycle transition to the pipe.
2142    #[cfg(feature = "telemetry-pipe")]
2143    pub fn notify_app_state(&self, state: AppState) {
2144        if let Some(pipe) = &self.telemetry_pipe {
2145            pipe.notify_app_state(state);
2146        }
2147    }
2148
2149    /// Hands the pipe the protocol-state store and the per-install id once
2150    /// `initialize_mls` has them. Adoption happens on the pipe's own thread.
2151    #[cfg(feature = "telemetry-pipe")]
2152    fn attach_telemetry_storage(&self) {
2153        let Some(pipe) = &self.telemetry_pipe else {
2154            return;
2155        };
2156        if let (Some(storage), Some(cipher)) =
2157            (&self.protocol_state_storage, &self.state_record_cipher)
2158        {
2159            pipe.attach_storage(storage.clone(), cipher.clone());
2160        }
2161        pipe.set_device_id(self.telemetry_install_id());
2162    }
2163
2164    /// Starts the protocol.
2165    ///
2166    /// # Returns
2167    ///
2168    /// Returns `Ok(())` if started successfully, `Err` if already started.
2169    pub fn start(&mut self) -> Result<()> {
2170        let state = lock_shared_state(&self.shared_state)?;
2171
2172        if state.state != ProtocolState::Stopped {
2173            return Err(Error::AlreadyStarted);
2174        }
2175
2176        // Start all transports
2177        drop(state);
2178        self.transport_manager.start()?;
2179
2180        // Apply the Nostr sealing kill switch here rather than at transport
2181        // construction: the transport is built by the bindings layer, so this
2182        // is the one place every caller — FFI, RN, and a pure-Rust embedder
2183        // that installed its own `NostrTransport` — passes through with the
2184        // config in hand. A no-op when Nostr is not installed.
2185        self.transport_manager
2186            .set_nostr_sealing_enabled(self.config.transport.nostr_sealing_enabled);
2187        self.transport_manager
2188            .set_nostr_cold_contact_enabled(self.config.transport.nostr_cold_contact_enabled);
2189        self.transport_manager.set_nostr_username_discovery_enabled(
2190            self.config.transport.nostr_username_discovery_enabled,
2191        );
2192
2193        // Wire DORS event callback so app receives dors_score_updated, dors_transport_selected,
2194        let shared = self.shared_state.clone();
2195        self.transport_manager
2196            .set_dors_event_callback(Some(Arc::new(move |event| {
2197                shared.lock_or_recover().emit_event(event);
2198            })));
2199
2200        // NOTE: the structured routing-decision callback is wired by
2201        // `install_telemetry_sink`, not here. Its lifetime is tied to sink
2202        // presence rather than protocol start/stop — see the docstring on
2203        // `install_telemetry_sink` for the rationale.
2204
2205        // Wire BLE fragment eviction callback so app receives FragmentAssemblyEvicted.
2206        if let Some(ble_arc) = self.transport_manager.get_transport(TransportType::BLE) {
2207            let shared = self.shared_state.clone();
2208            if let Some(ble) = ble_arc.as_any().downcast_ref::<BleTransport>() {
2209                ble.set_fragment_eviction_callback(Some(Arc::new(move |info| {
2210                    shared
2211                        .lock_or_recover()
2212                        .emit_event(Event::fragment_assembly_evicted(
2213                            info.message_id,
2214                            info.completion_percent,
2215                            "capacity".to_string(),
2216                        ));
2217                })));
2218            }
2219        }
2220
2221        let mut state = lock_shared_state(&self.shared_state)?;
2222
2223        state.state = ProtocolState::Running;
2224        drop(state);
2225
2226        // Settle what restore could not recover, now that the event pipeline is
2227        // live. These ids were handed to the app by `send_message*` before the
2228        // process died; without this they would never resolve to anything.
2229        // Drained before the flush below so an app sees the failures first and
2230        // cannot mistake a restored send for the settlement of a lost one.
2231        self.drain_deferred_restore_settlements();
2232
2233        self.flush_restored_confirmed_pending_messages();
2234
2235        // Cold start: sessions restored from storage are already confirmed,
2236        // so no confirmation transition will fire for them and nothing else
2237        // would offer versions until the peer happened to be rediscovered.
2238        // Frames toward an unreachable peer queue in the outbox, which is the
2239        // ladder doing its job rather than a reason not to try.
2240        #[cfg(feature = "data")]
2241        {
2242            // The space list is derived from the records themselves rather
2243            // than from the space index, because the index is a cache: a
2244            // document written without ever being explicitly created leaves
2245            // records but no index entry, and those are precisely the
2246            // documents no peer has seen yet.
2247            // Group spaces appear in this list and are deliberately not
2248            // swept: `kick_data_sync` answers only for a space named after
2249            // a peer, so a group space falls through it. A cold-start sweep
2250            // for groups would mean an offer per member per group at every
2251            // launch, and what it would recover is narrow — a local commit
2252            // is already pushed to the roster when it happens, and the
2253            // per-member outbox carries it across a restart. Rediscovery
2254            // and joins reconcile the rest.
2255            for space in self.data_list_spaces().unwrap_or_default() {
2256                self.kick_data_sync(&space, "start");
2257            }
2258        }
2259
2260        self.kick_pending_session_reconciliation("start");
2261        self.process_welcome_retry_queue()?;
2262
2263        // Re-drive delivery of any outbox entries restored from a previous
2264        // session. They land here in the "stranded" state (not in the retry
2265        // queue, not awaiting an ACK), which flush_outbox_all already recovers:
2266        // it sends immediately where a transport is available and re-enqueues
2267        // the rest with backoff. Runs at start() rather than at restore time
2268        // because transports aren't up yet during initialize_mls.
2269        self.flush_outbox_all();
2270
2271        // Media transfers that died with the previous process cannot be
2272        // re-driven — only their descriptors survive (never chunk bytes).
2273        // Tell the app which transfers to re-initiate, now that the event
2274        // pipeline is live. The descriptors stay parked (and persisted)
2275        // until the resend consumes them or the restore TTL prunes them,
2276        // so an app that misses this signal gets it again next restart.
2277        //
2278        // Except the layer's own. An application cannot answer one of those:
2279        // the only surface that takes a caller-supplied `file_id` is
2280        // `send_media_with`, which forces the purpose to `None`, so a
2281        // well-behaved app re-supplying the bytes would deliver a document
2282        // snapshot to the peer's user as a downloaded file. They are dropped
2283        // instead, and nothing is stranded: the requester's fetch expires and
2284        // can be asked again, and a snapshot is re-offered by the next
2285        // anti-entropy exchange.
2286        let (recoverable, internal): (Vec<_>, Vec<_>) = self
2287            .restored_media_descriptors
2288            .values()
2289            .cloned()
2290            .partition(|descriptor| descriptor.data_purpose.is_none());
2291        for descriptor in &internal {
2292            debug!(
2293                file_id = %descriptor.file_id,
2294                "Dropping an interrupted document-layer transfer; the asking side will retry"
2295            );
2296            self.restored_media_descriptors.remove(&descriptor.file_id);
2297            self.remove_media_descriptor(&descriptor.file_id);
2298        }
2299        for descriptor in recoverable {
2300            self.emit_event(Event::media_resend_required(
2301                descriptor.file_id.clone(),
2302                descriptor.recipient.clone(),
2303                descriptor.file_name.clone(),
2304                descriptor.file_size,
2305                descriptor.app_id.clone(),
2306            ));
2307        }
2308
2309        Ok(())
2310    }
2311
2312    /// Stops the protocol gracefully.
2313    ///
2314    /// # Returns
2315    ///
2316    /// Returns `Ok(())` if stopped successfully, `Err` if not started.
2317    pub fn stop(&mut self) -> Result<()> {
2318        let state = lock_shared_state(&self.shared_state)?;
2319
2320        if state.state == ProtocolState::Stopped {
2321            return Ok(()); // Already stopped
2322        }
2323
2324        // Flush debounced Lamport clock before stopping so no ticks are lost.
2325        drop(state);
2326        self.flush_lamport_clock();
2327        // Same for the Nostr receive watermark: an un-flushed tail costs the
2328        // next launch a wider replay window.
2329        self.flush_nostr_watermark();
2330        // And the deduplicator's seen set: an un-flushed tail is a message
2331        // whose second copy the next launch fails to recognise.
2332        self.flush_dedup_seen();
2333        // Answer any lookup still in flight while the transport is still up to
2334        // take the cancellations. Nothing pumps relays or sweeps deadlines once
2335        // stopped, so a resolution left here is an event that never comes.
2336        self.flush_username_resolutions_for_stop();
2337
2338        // Clear event callbacks to release shared_state references.
2339        // NOTE: the routing-decision callback is deliberately NOT cleared
2340        // here. Its lifetime is sink-scoped (installed by
2341        // `install_telemetry_sink`, replaced by a subsequent install),
2342        // not protocol-running-scoped — so a `stop() → start()` cycle
2343        // preserves the wiring without requiring the app to re-install.
2344        self.transport_manager.set_dors_event_callback(None);
2345        if let Some(ble_arc) = self.transport_manager.get_transport(TransportType::BLE) {
2346            if let Some(ble) = ble_arc.as_any().downcast_ref::<BleTransport>() {
2347                ble.set_fragment_eviction_callback(None);
2348            }
2349        }
2350
2351        self.transport_manager.stop()?;
2352        let mut state = lock_shared_state(&self.shared_state)?;
2353
2354        state.state = ProtocolState::Stopped;
2355
2356        Ok(())
2357    }
2358
2359    /// Pauses the protocol (for background mode).
2360    pub fn pause(&mut self) -> Result<()> {
2361        self.flush_lamport_clock();
2362        self.flush_nostr_watermark();
2363        let mut state = lock_shared_state(&self.shared_state)?;
2364
2365        if state.state != ProtocolState::Running {
2366            return Err(Error::NotStarted);
2367        }
2368
2369        state.state = ProtocolState::Paused;
2370        Ok(())
2371    }
2372
2373    /// Resumes the protocol from pause.
2374    pub fn resume(&mut self) -> Result<()> {
2375        {
2376            let mut state = lock_shared_state(&self.shared_state)?;
2377
2378            if state.state != ProtocolState::Paused {
2379                return Err(Error::InvalidConfiguration(
2380                    "Protocol is not paused".to_string(),
2381                ));
2382            }
2383
2384            state.state = ProtocolState::Running;
2385        }
2386
2387        // A pause is the other edge back into a live event pipeline, so it owes
2388        // the same drain `start()` does. `settle_restored_message_failure` parks
2389        // anything it produces while the protocol is not `Running`, and
2390        // `update_retry_config` reaches it at runtime: shortening
2391        // `pending_message_max_lifetime_ms` in the background expires queued
2392        // messages and parks their terminal `message_failed`. Without this the
2393        // app would hold those ids until a `start()` that may never come again.
2394        self.drain_deferred_restore_settlements();
2395        Ok(())
2396    }
2397
2398    /// Emits every terminal settlement parked while the event pipeline was not
2399    /// live, and reports the count of any the cap dropped.
2400    ///
2401    /// Called from both edges into `Running` ([`Self::start`] and
2402    /// [`Self::resume`]) so a parked settlement has no state to be stranded in.
2403    /// Idempotent: draining an empty queue emits nothing.
2404    fn drain_deferred_restore_settlements(&mut self) {
2405        for (event, telemetry) in std::mem::take(&mut self.deferred_restore_settlements) {
2406            self.emit_settlement(event, telemetry);
2407        }
2408        let suppressed = std::mem::take(&mut self.suppressed_restore_settlements);
2409        if suppressed > 0 {
2410            warn!(
2411                suppressed,
2412                cap = MAX_DEFERRED_RESTORE_SETTLEMENTS,
2413                "Restore produced more terminal settlements than any legitimate run can produce; \
2414                 the ids past the cap were not reported individually"
2415            );
2416        }
2417    }
2418
2419    /// Forget that a peer replicates documents.
2420    ///
2421    /// The capability and the offer window are two halves of one fact, and
2422    /// they have to be dropped together: a window left behind outlives the
2423    /// capability and then suppresses the first offer to the same peer once
2424    /// it is relearned, which is a document that silently does not sync.
2425    pub(crate) fn forget_data_sync_peer(&mut self, peer: &str) {
2426        self.peer_data_sync.remove(peer);
2427        self.peer_data_group.remove(peer);
2428        self.peer_data_group_attested.remove(peer);
2429        self.peer_data_media.remove(peer);
2430        self.peer_data_tombstones.remove(peer);
2431        self.peer_data_interest.remove(peer);
2432        self.peer_data_group_blob.remove(peer);
2433        self.peer_data_group_blob_attested.remove(peer);
2434        self.peer_data_custody.remove(peer);
2435        // A peer we have stopped replicating with cannot answer anything we
2436        // asked them for, so the questions go too. Left behind they would
2437        // hold slots against the fetch bound until they timed out.
2438        #[cfg(feature = "data")]
2439        self.forget_attachment_state(peer);
2440        #[cfg(feature = "data")]
2441        self.forget_data_sync_offer_windows(peer);
2442    }
2443
2444    /// [`Self::forget_data_sync_peer`] for every peer at once.
2445    pub(crate) fn forget_every_data_sync_peer(&mut self) {
2446        self.peer_data_sync.clear();
2447        self.peer_data_group.clear();
2448        self.peer_data_group_attested.clear();
2449        self.peer_data_media.clear();
2450        self.peer_data_tombstones.clear();
2451        self.peer_data_interest.clear();
2452        self.peer_data_group_blob.clear();
2453        self.peer_data_group_blob_attested.clear();
2454        // Reported, not merely dropped, exactly as the single-peer road
2455        // reports. Nothing the application did reaches this one: the bound
2456        // on remembered peers is hit, a stranger's key package forgets every
2457        // capability at once, and a fetch outstanding at that moment ends
2458        // for a reason its asker has no way to see. Dropping the record
2459        // silently also forecloses the expiry that would have reported it
2460        // later, so the spinner this event exists to end never ends at all.
2461        #[cfg(feature = "data")]
2462        self.end_every_pending_fetch();
2463        self.blob_request_windows.clear();
2464        #[cfg(feature = "data")]
2465        self.last_data_sync_offer.clear();
2466    }
2467
2468    /// Called when a new neighbor is discovered.
2469    ///
2470    /// When auto key exchange is enabled, this method sends our key package
2471    /// to the newly discovered peer to enable encrypted communication.
2472    ///
2473    /// # Arguments
2474    ///
2475    /// * `peer_id` - The ID of the discovered peer
2476    pub fn on_neighbor_discovered(&mut self, peer_id: &str) {
2477        self.on_neighbor_discovered_via(peer_id, None);
2478    }
2479
2480    /// [`Self::on_neighbor_discovered`] with an optional transport override
2481    /// for the outbox flush (see [`Self::flush_outbox_for_peer_via`]).
2482    /// `unpark_via` is `Some(Internet)` when the relay presence-online edge
2483    /// re-enters discovery through its rescue branch: a message whose
2484    /// forced-internet re-drive just failed sits in the retry queue with no
2485    /// pending ACK, so this inner flush would pick it up again — and
2486    /// without the override DORS could route it into the mesh void with the
2487    /// park counter already cleared, past re-park's reach.
2488    fn on_neighbor_discovered_via(&mut self, peer_id: &str, unpark_via: Option<TransportType>) {
2489        // Don't track ourselves
2490        if peer_id == self.local_id {
2491            return;
2492        }
2493
2494        // Don't track or auto-exchange keys with blocked users
2495        if self.is_user_blocked(peer_id) {
2496            debug!(peer_id = %peer_id, "Ignoring neighbor discovery for blocked user");
2497            return;
2498        }
2499
2500        // Track discovered peers for service discovery and routing. Existing
2501        // peers get their last-seen refreshed; a new peer at capacity evicts
2502        // the least-recently-seen entry so a genuinely present neighbor is
2503        // never locked out by stale message-path senders (issue #140).
2504        if let Some(last_seen) = self.known_peers.get_mut(peer_id) {
2505            *last_seen = Instant::now();
2506        } else {
2507            if self.known_peers.len() >= MAX_KNOWN_PEERS {
2508                if let Some(victim) = self
2509                    .known_peers
2510                    .iter()
2511                    .min_by_key(|(_, seen)| **seen)
2512                    .map(|(id, _)| id.clone())
2513                {
2514                    debug!(peer_id = %peer_id, evicted = %victim, cap = MAX_KNOWN_PEERS, "Known peers at capacity, evicting least-recently-seen");
2515                    self.evict_known_peer(&victim);
2516                }
2517            }
2518            self.known_peers.insert(peer_id.to_string(), Instant::now());
2519        }
2520
2521        // Flush any pending outbox messages destined for this peer
2522        self.flush_outbox_for_peer_via(peer_id, unpark_via);
2523
2524        // Held frames for this neighbour, or to try through it
2525        // (`docs/spec/custody.md`, "Redelivery").
2526        self.redeliver_custody_to(peer_id);
2527
2528        // A Welcome that stalled or expired while this peer was unreachable now
2529        // has a fresh delivery opportunity over the carrier that surfaced this
2530        // peer — re-arm it. No-op when there is no pending Welcome for the peer.
2531        self.rearm_welcome_for_peer(peer_id, "peer_rediscovered");
2532
2533        // Offer document versions while the peer is in front of us. This has
2534        // to sit above the key-package guard below, which returns early for
2535        // any peer we have already exchanged with — that is to say, for
2536        // exactly the established peers whose documents drifted apart during
2537        // the partition this discovery just ended.
2538        #[cfg(feature = "data")]
2539        self.kick_data_sync(peer_id, "peer_rediscovered");
2540
2541        // The same reconciliation for every group space shared with this
2542        // peer. A group space cannot ride the 1:1 trigger above: two
2543        // members of a group need not have a session with each other at
2544        // all, so for them this discovery is the only moment anything knows
2545        // they are both reachable.
2546        #[cfg(feature = "data")]
2547        self.kick_shared_group_data_sync(peer_id, "peer_rediscovered");
2548
2549        // Only send key package if encryption is enabled and auto key exchange is on
2550        if !self.config.encryption.enabled || !self.config.encryption.auto_key_exchange {
2551            return;
2552        }
2553
2554        // The first push is discovery's. A later discovery re-arms it only
2555        // when that push produced no session, at most once per interval.
2556        if self.key_package_sent_to.contains_key(peer_id) {
2557            self.rearm_key_package_for_peer(peer_id);
2558            return;
2559        }
2560
2561        // Only if MLS is initialized
2562        if self.mls_manager.is_none() {
2563            return;
2564        }
2565
2566        if let Err(e) = self.send_key_package_to(peer_id, false) {
2567            warn!(error = %e, peer_id = %peer_id, "Failed to send key package on discovery");
2568        }
2569    }
2570
2571    /// Immediately attempts to send all pending outbox messages destined
2572    /// for a specific peer, bypassing backoff timers. Called on per-peer
2573    /// reachability edges (discovery, relay presence-online) to flush
2574    /// messages that were queued while the peer was unreachable.
2575    ///
2576    /// `unpark_via` is an optional transport override for the re-driven
2577    /// sends: `Some(Internet)` on the relay presence-online edge
2578    /// (`on_peer_presence`, including its rescue branch's re-entry through
2579    /// `on_neighbor_discovered_via`): the reachability
2580    /// proof is relay-scoped, so the re-drive must go out over the internet
2581    /// transport — DORS could otherwise route it back into the mesh, where
2582    /// the send locally succeeds against a peer that is not there,
2583    /// re-registering an unanswerable ACK that re-strands the message (and,
2584    /// with the platform bridge unwatching the peer on the online answer,
2585    /// no further presence edge would arrive to save it). Pinned media
2586    /// transports still win over the override.
2587    fn flush_outbox_for_peer_via(&mut self, peer_id: &str, unpark_via: Option<TransportType>) {
2588        // Probe ACKs first: with a live park counter, the peer's DMs may be
2589        // mid-probe — awaiting a probe ACK that may never arrive (never, over
2590        // a mesh carrier) — and the awaiting-ACK filter below would skip
2591        // exactly the messages this edge exists to re-drive.
2592        self.cancel_probe_state_for_parked_peer(peer_id);
2593        // A per-peer reachability edge: the escalating unreachable-probe
2594        // interval starts over (mirrors rearm_welcome_for_peer resetting
2595        // unreachable_parks). Snapshotted, not dropped: an edge whose
2596        // re-drives all fail didn't actually take, and restores it below.
2597        let prior_parks = self.dm_unreachable_parks.remove(peer_id);
2598
2599        // Collect matching messages from outbox + media_outbox
2600        let mut to_send: Vec<(Message, u32)> = Vec::new();
2601
2602        for entry in self.outbox.values() {
2603            if entry.message.recipient.as_str() == peer_id
2604                && !self.ack_manager.is_waiting_for_ack(&entry.message.id)
2605            {
2606                to_send.push((entry.message.clone(), entry.attempt_count));
2607            }
2608        }
2609        for entry in self.media_outbox.values() {
2610            if entry.message.recipient.as_str() == peer_id
2611                && !self.ack_manager.is_waiting_for_ack(&entry.message.id)
2612            {
2613                to_send.push((entry.message.clone(), entry.attempt_count));
2614            }
2615        }
2616
2617        if to_send.is_empty() {
2618            return;
2619        }
2620
2621        let count = to_send.len().min(crate::constants::FLUSH_BATCH_LIMIT);
2622        debug!(peer_id = %peer_id, count = count, "Flushing outbox for discovered peer");
2623
2624        let mut iter = to_send.into_iter();
2625        let mut parked_dm_seen = false;
2626        let mut parked_dm_redriven = false;
2627
2628        // Only process up to FLUSH_BATCH_LIMIT messages per edge.
2629        for (message, attempt_count) in iter.by_ref().take(crate::constants::FLUSH_BATCH_LIMIT) {
2630            let parkable = self.is_parkable_plain_dm(&message.id);
2631            // Remove from retry queue since we're sending immediately
2632            self.retry_queue.remove(&message.id.as_str());
2633            let sent = self.try_flush_send_via(message, attempt_count, unpark_via);
2634            if parkable {
2635                parked_dm_seen = true;
2636                parked_dm_redriven |= sent.is_some();
2637            }
2638        }
2639
2640        // Re-enqueue the overflow (mirrors flush_outbox_all): the probe
2641        // cancel above may have stripped a parked message's retry entry, so
2642        // "still holds its backoff timer" cannot be assumed past the batch
2643        // limit. Enqueue dedupes by id — entries still scheduled keep their
2644        // existing timer, canceled ones get a fresh backoff slot instead of
2645        // stranding edge-only with the park counter cleared.
2646        for (message, attempt_count) in iter {
2647            parked_dm_seen = parked_dm_seen || self.is_parkable_plain_dm(&message.id);
2648            let _ = self.retry_queue.enqueue(message, attempt_count);
2649        }
2650
2651        // An edge that re-drove no parkable DM at all didn't actually take:
2652        // the DMs now sit in the retry queue, whose later sends are
2653        // DORS-routed (no transport override there), and a send that succeeds
2654        // locally without reaching the peer would re-register an ACK nothing
2655        // will answer. Restore the counter so exhaustion stays within
2656        // `try_repark_exhausted_dm`'s reach instead of settling terminally
2657        // with the counter cleared.
2658        //
2659        // Restore granularity is per-peer, matching the counter: one
2660        // successful re-drive clears it even when a sibling's send failed on
2661        // the same edge, leaving that sibling on DORS-routed backoff exposed
2662        // to terminal exhaustion. Accepted gap — it requires one transport
2663        // to both succeed and fail within a single flush loop.
2664        if let Some(parks) = prior_parks {
2665            if parked_dm_seen && !parked_dm_redriven {
2666                self.dm_unreachable_parks.insert(peer_id.to_string(), parks);
2667            }
2668        }
2669    }
2670
2671    /// Immediately attempts to send all pending outbox messages across all peers.
2672    ///
2673    /// Called when a transport becomes available (e.g. internet reconnects) to
2674    /// flush all queued messages, bypassing backoff timers.
2675    pub fn flush_outbox_all(&mut self) {
2676        // Acknowledgements we owe first: they are what stop peers re-sending
2677        // to us, and they have no outbox entry for the walk below to find.
2678        self.flush_unrouted_acks();
2679        // Probe ACKs first (see `flush_outbox_for_peer_via`): mid-probe DMs
2680        // are awaiting unanswerable mesh ACKs, and the awaiting-ACK guard
2681        // below would strand them past this edge with their counters
2682        // cleared. Canceled entries surface through the stranded-outbox
2683        // collection.
2684        let parked_peers: Vec<String> = self.dm_unreachable_parks.keys().cloned().collect();
2685        for peer_id in parked_peers {
2686            self.cancel_probe_state_for_parked_peer(&peer_id);
2687        }
2688        // A carrier-level reachability edge (reconnect / start): every
2689        // escalating unreachable-probe interval starts over. Snapshotted,
2690        // not dropped: a peer whose re-drives all fail gets its counter
2691        // restored below (see `flush_outbox_for_peer_via`).
2692        let prior_parks = std::mem::take(&mut self.dm_unreachable_parks);
2693
2694        // Drain all entries from the retry queue (ignores timing)
2695        let retry_entries = self.retry_queue.drain_all();
2696
2697        // Also collect outbox entries NOT in the retry queue (stranded after
2698        // previous max_retries rejection)
2699        let mut all_messages: Vec<(Message, u32)> = retry_entries
2700            .into_iter()
2701            .map(|e| (e.message, e.retry_count))
2702            .collect();
2703
2704        // Add outbox entries that weren't in the retry queue (stranded) AND
2705        // aren't already waiting for an ACK (successfully sent, just awaiting
2706        // confirmation). Without the ACK check, a prior flush that succeeded
2707        // would cause these messages to be re-sent unnecessarily.
2708        let retry_ids: std::collections::HashSet<String> =
2709            all_messages.iter().map(|(m, _)| m.id.as_str()).collect();
2710
2711        for entry in self.outbox.values() {
2712            if !retry_ids.contains(&entry.message.id.as_str())
2713                && !self.ack_manager.is_waiting_for_ack(&entry.message.id)
2714            {
2715                all_messages.push((entry.message.clone(), entry.attempt_count));
2716            }
2717        }
2718        for entry in self.media_outbox.values() {
2719            if !retry_ids.contains(&entry.message.id.as_str())
2720                && !self.ack_manager.is_waiting_for_ack(&entry.message.id)
2721            {
2722                all_messages.push((entry.message.clone(), entry.attempt_count));
2723            }
2724        }
2725
2726        // Opt-in (`edge_driven_unreachable_dm`): on the start/reconnect re-drive,
2727        // leave durably-failing messages (first sent longer ago than
2728        // OUTBOX_DURABLE_UNREACHABLE_AGE_SECS, still undelivered) edge-driven
2729        // rather than re-driving them, so a restart does not re-ramp a large
2730        // dead-peer backlog. They rest in the outbox and flush the instant their
2731        // peer next proves reachable. Default-off preserves the documented
2732        // "reconnect re-drives, ignoring timing" behaviour for every existing
2733        // app. drain_all() already removed these from the retry queue and the
2734        // outbox entry is untouched, so "not collected" == "resting edge-driven".
2735        if self.config.reliability.retry.edge_driven_unreachable_dm {
2736            let now = Utc::now();
2737            let durable_age = chrono::Duration::seconds(
2738                crate::protocol::types::OUTBOX_DURABLE_UNREACHABLE_AGE_SECS,
2739            );
2740            all_messages.retain(|(m, _)| {
2741                match self
2742                    .outbox
2743                    .get(&m.id)
2744                    .or_else(|| self.media_outbox.get(&m.id))
2745                    .map(|e| e.first_sent_at)
2746                {
2747                    Some(first_sent) => now.signed_duration_since(first_sent) < durable_age,
2748                    None => true,
2749                }
2750            });
2751        }
2752
2753        if all_messages.is_empty() {
2754            return;
2755        }
2756
2757        let count = all_messages.len().min(crate::constants::FLUSH_BATCH_LIMIT);
2758        debug!(
2759            count = count,
2760            total = all_messages.len(),
2761            "Flushing all outbox messages"
2762        );
2763
2764        let mut iter = all_messages.into_iter();
2765        let mut parked_dm_seen: std::collections::HashSet<String> =
2766            std::collections::HashSet::new();
2767        let mut parked_dm_redriven: std::collections::HashSet<String> =
2768            std::collections::HashSet::new();
2769
2770        for (message, attempt_count) in iter.by_ref().take(crate::constants::FLUSH_BATCH_LIMIT) {
2771            let recipient = message.recipient.as_str().to_string();
2772            let parkable =
2773                prior_parks.contains_key(&recipient) && self.is_parkable_plain_dm(&message.id);
2774            let sent_via = self.try_flush_send(message, attempt_count);
2775            if parkable {
2776                // Unlike the per-peer edges (discovery carries mesh-scoped
2777                // proof, presence-online forces the answering carrier), this
2778                // carrier-level edge re-drives via DORS with no per-peer
2779                // reachability proof: a mesh-routed send "locally succeeds"
2780                // into the mesh whether or not the peer is there, so only a
2781                // send over a carrier that answers verdicts counts as
2782                // re-driven for counter-clearing — that is the send whose
2783                // failure would say something. A mesh-routed success keeps
2784                // the counter restore-eligible; if its ACK genuinely
2785                // arrives, delivery prunes the counter.
2786                if sent_via.and_then(GatewayCarrier::from_transport).is_some() {
2787                    parked_dm_redriven.insert(recipient);
2788                } else {
2789                    parked_dm_seen.insert(recipient);
2790                }
2791            }
2792        }
2793
2794        // Re-enqueue any messages beyond the batch limit so they aren't lost
2795        for (message, attempt_count) in iter {
2796            let recipient = message.recipient.as_str();
2797            if prior_parks.contains_key(recipient) && self.is_parkable_plain_dm(&message.id) {
2798                parked_dm_seen.insert(recipient.to_string());
2799            }
2800            let _ = self.retry_queue.enqueue(message, attempt_count);
2801        }
2802
2803        // Restore the counter of every parked peer none of whose DMs were
2804        // re-driven on this edge (see `flush_outbox_for_peer_via`): their
2805        // retry-queue sends are DORS-routed, and a send that succeeds locally
2806        // without reaching the peer must leave exhaustion re-parkable rather
2807        // than terminal.
2808        for (peer_id, parks) in prior_parks {
2809            if parked_dm_seen.contains(&peer_id) && !parked_dm_redriven.contains(&peer_id) {
2810                self.dm_unreachable_parks.insert(peer_id, parks);
2811            }
2812        }
2813    }
2814
2815    /// Records what the attached gateway daemon says it can do.
2816    ///
2817    /// Wholesale replace, like the relay's: each attach describes the gateway
2818    /// actually connected now. The bridge calls this **before** it reports the
2819    /// carrier available, which is the contract's own ordering — a device must
2820    /// not drain into a gateway whose features it has not been told — and it
2821    /// clears the set when the carrier drops.
2822    ///
2823    /// The list is wire-supplied, so it is bounded exactly as the relay's is,
2824    /// which is what the contract points at rather than inventing a second
2825    /// pair of numbers: at most 64 tokens of at most 128 bytes each, the same
2826    /// `MAX_RELAY_CAPABILITIES` and `MAX_RELAY_CAPABILITY_TOKEN_BYTES` the
2827    /// relay's advertisement is bounded by. Oversized tokens are
2828    /// dropped *before* the count is applied, so padding cannot evict real
2829    /// ones.
2830    ///
2831    /// Nothing here gates a security decision. Tokens gate features, and an
2832    /// attacker who can set them is already the gateway.
2833    pub fn set_gateway_capabilities(&mut self, capabilities: Vec<String>) {
2834        let caps: HashSet<String> = capabilities
2835            .into_iter()
2836            .filter(|c| !c.is_empty() && c.len() <= MAX_RELAY_CAPABILITY_TOKEN_BYTES)
2837            .take(MAX_RELAY_CAPABILITIES)
2838            .collect();
2839        debug!(count = caps.len(), "Gateway capabilities updated");
2840        self.gateway_capabilities = caps;
2841    }
2842
2843    /// Forgets the attached gateway's advertisement, on carrier drop.
2844    pub fn clear_gateway_capabilities(&mut self) {
2845        if !self.gateway_capabilities.is_empty() {
2846            debug!("Gateway capabilities cleared on carrier drop");
2847            self.gateway_capabilities.clear();
2848        }
2849    }
2850
2851    /// The attached gateway's advertised capability tokens, sorted so the
2852    /// order a caller sees does not depend on hashing.
2853    pub fn gateway_capabilities(&self) -> Vec<String> {
2854        let mut tokens: Vec<String> = self.gateway_capabilities.iter().cloned().collect();
2855        tokens.sort();
2856        tokens
2857    }
2858
2859    /// Peers the platform layer should watch via a gateway `CheckPresence`
2860    /// query: every peer with an undelivered or session-unproven MLS welcome
2861    /// ([`Self::welcome_pending_peers`]) plus every recipient of an outbox
2862    /// message not currently awaiting an ACK — which includes DMs parked on a
2863    /// `recipient_unreachable` verdict. A gateway's presence-online answer
2864    /// lands in [`Self::on_peer_presence_via`], whose
2865    /// `flush_outbox_for_peer_via` is what re-drives (un-parks) those
2866    /// messages, so the SDK — not the app — owns the "watch my DeliveryError
2867    /// recipients" duty. Bounded by the outbox cap (500 entries).
2868    ///
2869    /// Carrier-agnostic on purpose: the set is "who this device is waiting to
2870    /// hear about", which does not depend on who is asked. Every attached
2871    /// gateway watches the same peers.
2872    pub fn presence_watch_peers(&self) -> Vec<String> {
2873        let mut peers = self.welcome_pending_peers();
2874        let mut seen: std::collections::HashSet<String> = peers.iter().cloned().collect();
2875        for entry in self.outbox.values() {
2876            if self.ack_manager.is_waiting_for_ack(&entry.message.id) {
2877                continue;
2878            }
2879            let recipient = entry.message.recipient.as_str();
2880            if seen.insert(recipient.to_string()) {
2881                peers.push(recipient.to_string());
2882            }
2883        }
2884        peers
2885    }
2886
2887    /// Attempts to send a single message as part of a flush operation.
2888    ///
2889    /// Ensures the outbox entry exists before sending. A message reaching here
2890    /// normally has one: every path that gives a message up retires its retry
2891    /// entry with it (`retire_undeliverable_message`), so an eviction no longer
2892    /// leaves one behind to resurrect here, and re-creating a settled message
2893    /// would evict a live one at capacity. On success,
2894    /// registers ACK tracking and updates the outbox entry. On failure,
2895    /// re-enqueues the message to the retry queue with its current attempt count
2896    /// so backoff resumes. Returns the transport the send went out over, or
2897    /// `None` when it failed (or, defensively, when the transport could not
2898    /// be attributed — the park restore logic treats both as not-redriven,
2899    /// the safe direction; in practice a successful send always has one).
2900    fn try_flush_send(&mut self, message: Message, attempt_count: u32) -> Option<TransportType> {
2901        self.try_flush_send_via(message, attempt_count, None)
2902    }
2903
2904    /// [`Self::try_flush_send`] with an optional transport override (see
2905    /// [`Self::flush_outbox_for_peer_via`]). A pinned media transport takes
2906    /// precedence over the override; without either, DORS picks.
2907    fn try_flush_send_via(
2908        &mut self,
2909        mut message: Message,
2910        attempt_count: u32,
2911        transport_override: Option<TransportType>,
2912    ) -> Option<TransportType> {
2913        // No slot to track it: leave the send to the retry queue, which waits
2914        // for one, instead of transmitting a frame nobody tracks.
2915        if message.requires_ack && !self.ack_manager.has_capacity_for(&message.id) {
2916            self.ensure_outbox_entry(&message);
2917            let _ = self.retry_queue.enqueue(message, attempt_count);
2918            return None;
2919        }
2920        // A full peer window holds a frame that has not gone out yet; the
2921        // window flush sends it when an acknowledgement frees a slot.
2922        if !self.peer_window_admits(&message) {
2923            self.hold_for_peer_window(&message);
2924            return None;
2925        }
2926        self.ensure_outbox_entry(&message);
2927        // Tier 2: re-seal against the peer's current session before flushing, so
2928        // a post-re-key epoch change no longer leaves the bytes undecryptable.
2929        // No-op for media/plaintext/unconfirmed sessions.
2930        self.reseal_for_resend_in_place(&mut message);
2931        let forced_transport = self
2932            .pinned_media_transport_for_message(&message.id)
2933            .or(transport_override);
2934        let send_result = if let Some(transport) = forced_transport {
2935            self.transport_manager
2936                .send_via_transport(&message, transport)
2937        } else {
2938            self.transport_manager.send(&message)
2939        };
2940        let current_transport =
2941            forced_transport.or_else(|| self.transport_manager.current_transport());
2942
2943        match send_result {
2944            Ok(()) => {
2945                if let Err(e) = self.ensure_ack_registration(&message) {
2946                    warn!(message_id = %message.id, error = %e, "ACK registration failed during flush");
2947                }
2948                self.mark_message_sent(
2949                    &message,
2950                    current_transport,
2951                    Some(attempt_count.saturating_add(1)),
2952                );
2953                self.offer_resend_to_mesh(&message, true);
2954                debug!(message_id = %message.id, "Flush send succeeded");
2955                current_transport
2956            }
2957            Err(e) => {
2958                self.offer_resend_to_mesh(&message, false);
2959                let _ = self.retry_queue.enqueue(message.clone(), attempt_count);
2960                debug!(message_id = %message.id, error = %e, "Flush send failed, re-enqueued");
2961                None
2962            }
2963        }
2964    }
2965
2966    /// Sends held messages into each peer's free window slots, oldest first,
2967    /// at most a flush batch per peer per tick.
2968    ///
2969    /// Runs every tick rather than from the acknowledgement handler alone,
2970    /// because a slot is also freed by give-up, expiry and eviction, and a
2971    /// peer whose mesh link went away stops being governed by the window, so
2972    /// everything held for it goes to transport selection.
2973    fn flush_peer_windows(&mut self) {
2974        if self.window_held.is_empty() {
2975            return;
2976        }
2977        let peers: Vec<String> = self.window_held.keys().cloned().collect();
2978        for peer in peers {
2979            let mut budget = crate::constants::FLUSH_BATCH_LIMIT;
2980            while budget > 0 {
2981                let Some(message_id) = self
2982                    .window_held
2983                    .get(&peer)
2984                    .and_then(|held| held.front().cloned())
2985                else {
2986                    break;
2987                };
2988                let Some((message, attempt_count)) = self
2989                    .outbox
2990                    .get(&message_id)
2991                    .map(|entry| (entry.message.clone(), entry.attempt_count))
2992                else {
2993                    // Settled, expired or evicted while held.
2994                    if let Some(held) = self.window_held.get_mut(&peer) {
2995                        held.pop_front();
2996                    }
2997                    continue;
2998                };
2999                if !self.peer_window_admits(&message) {
3000                    break;
3001                }
3002                if let Some(held) = self.window_held.get_mut(&peer) {
3003                    held.pop_front();
3004                }
3005                budget -= 1;
3006                let _ = self.try_flush_send_via(message, attempt_count, None);
3007            }
3008            if self
3009                .window_held
3010                .get(&peer)
3011                .is_some_and(|held| held.is_empty())
3012            {
3013                self.window_held.remove(&peer);
3014            }
3015        }
3016    }
3017
3018    /// Called when a neighbor is lost.
3019    ///
3020    /// Cleans up tracking state for the lost peer.
3021    ///
3022    /// # Arguments
3023    ///
3024    /// * `peer_id` - The ID of the lost peer
3025    pub fn on_neighbor_lost(&mut self, peer_id: &str) {
3026        self.evict_known_peer(peer_id);
3027    }
3028
3029    /// Drops a peer from discovery tracking. Shared by explicit neighbor
3030    /// loss, the TTL sweep, and least-recently-seen eviction at capacity —
3031    /// all three mean "treat this peer as gone until re-seen", so all three
3032    /// also clear the key-package marker, letting a re-appearing peer
3033    /// receive a fresh key package exactly as on a BLE reconnect.
3034    fn evict_known_peer(&mut self, peer_id: &str) {
3035        self.key_package_sent_to.remove(peer_id);
3036        self.known_peers.remove(peer_id);
3037        // Release the forwarding allowance held for this link. A peer that
3038        // reconnects starts with a full allowance, which is correct: the limit
3039        // exists to pace a live link, not to punish one that came back.
3040        self.mesh_relay.forget_peer(peer_id);
3041    }
3042
3043    /// Tears down the 1:1 session with `peer_id` and rebuilds it, advancing
3044    /// post-compromise security.
3045    ///
3046    /// # Why an application has to ask
3047    ///
3048    /// Post-compromise security arrives when a commit rotates a member's leaf
3049    /// in the ratchet tree. This engine originates one on a re-key, and until
3050    /// now the only thing that drove a re-key was an **epoch desync**: a
3051    /// healthy pair that never forked never rotated, so the window an attacker
3052    /// holding old key material keeps open was bounded by nothing.
3053    ///
3054    /// That is felt hardest against a leaf node, which never commits at all, so
3055    /// every rotation in such a pair is the phone's to originate. It is not
3056    /// exclusive to one: two phones that simply never desync are in the same
3057    /// position.
3058    ///
3059    /// The cadence is the application's because this engine cannot pick one.
3060    /// A rotation costs a teardown, a key-package exchange and a re-establish,
3061    /// and what that is worth depends on the deployment: a door lock on mains
3062    /// power and a phone on a metered link do not want the same interval, and
3063    /// nothing on the wire says which one is on the other end.
3064    ///
3065    /// # What the peer sees
3066    ///
3067    /// Exactly what a desync-driven re-key sends, because it is the same code:
3068    /// a `__MLS_KEY_PKG__` frame carrying `session_reset`. A peer running this
3069    /// engine discards its stale session and converges in one round trip; a
3070    /// leaf node discards its session, mints a fresh key package and re-pairs.
3071    /// Queued plaintext survives, because it is sealed at flush time against
3072    /// whatever session is current then.
3073    ///
3074    /// # Rate limit
3075    ///
3076    /// Shared with the desync path, one re-key per peer per window. A caller
3077    /// looping on this cannot do to a pair what that bound exists to stop an
3078    /// attacker doing.
3079    ///
3080    /// # Returns
3081    ///
3082    /// * `Ok(true)` - the reset was advertised and the session torn down.
3083    /// * `Ok(false)` - the rate-limit window has not lapsed; nothing was done,
3084    ///   and a later call succeeds. Not an error: a caller on a fixed schedule
3085    ///   that briefly runs faster than the floor is behaving correctly.
3086    ///
3087    /// # A rotation that fails changes nothing
3088    ///
3089    /// The reset is advertised **before** the local session is torn down, so
3090    /// an `Err` leaves the pair exactly as it was: the session is still there,
3091    /// still usable, and the rate-limit window is not spent. Rotate while the
3092    /// peer is reachable, and treat a failure as "try again later" rather than
3093    /// as a session to rebuild.
3094    ///
3095    /// The order matters most against a leaf node, which speaks only when
3096    /// spoken to. Tearing down first and failing to send would leave this side
3097    /// with no session, the device holding one it will never be told to drop,
3098    /// and nothing on either side that re-opens the exchange.
3099    ///
3100    /// # Errors
3101    ///
3102    /// * [`Error::MlsNotInitialized`] - encryption is not set up.
3103    /// * [`Error::UserBlocked`] - the peer is blocked.
3104    /// * [`Error::Mls`] with `SessionNotFound` - there is no session to rebuild.
3105    ///   Establishing a first session is [`Self::establish_secure_session`];
3106    ///   this heals one that exists.
3107    /// * [`Error::Transport`] - no transport accepted the reset frame. The
3108    ///   session is untouched, as above.
3109    pub fn rekey_session(&mut self, peer_id: &str) -> Result<bool> {
3110        if self.is_user_blocked(peer_id) {
3111            return Err(Error::UserBlocked(peer_id.to_string()));
3112        }
3113        if self.mls_manager.is_none() {
3114            return Err(Error::MlsNotInitialized);
3115        }
3116        // A session has to exist for there to be one to heal. Checked before
3117        // the slot is claimed, so a refused call does not spend the window and
3118        // leave a real re-key waiting on it.
3119        if !self.has_mls_session(peer_id)? {
3120            return Err(Error::Mls(offline_protocol_mls::MlsError::SessionNotFound(
3121                peer_id.to_string(),
3122            )));
3123        }
3124        if !self.claim_rekey_slot(peer_id) {
3125            debug!(
3126                peer_id = %peer_id,
3127                "rekey_session: the rate-limit window has not lapsed"
3128            );
3129            return Ok(false);
3130        }
3131        if let Err(err) = self.drive_session_rekey(peer_id, "requested by the application") {
3132            // Nothing was sent and nothing was torn down, so nothing was
3133            // spent: a caller back in range a second from now is asking for a
3134            // rotation this pair has not had.
3135            self.release_rekey_slot(peer_id);
3136            return Err(err);
3137        }
3138        Ok(true)
3139    }
3140
3141    /// Establishes a secure MLS session with a peer.
3142    ///
3143    /// This high-level method handles the complete session establishment flow:
3144    /// 1. Checks if a session already exists (returns Ok(None) if so)
3145    /// 2. Checks for a pending key package from the peer
3146    /// 3. If found, imports the key package, creates the session, and sends the Welcome message
3147    /// 4. If no key package is available, asks the Nostr relays for one the
3148    ///    peer published (with cold contact on) and returns `SessionNotReady`
3149    ///
3150    /// This method is designed for use by application code that needs explicit control
3151    /// over session establishment, as opposed to the automatic encryption flow.
3152    ///
3153    /// # Arguments
3154    ///
3155    /// * `peer_id` - The ID of the peer to establish a session with
3156    ///
3157    /// # Returns
3158    ///
3159    /// * `Ok(Some(WelcomeMessage))` - Session created, Welcome message returned (and sent to peer)
3160    /// * `Ok(None)` - Session already exists
3161    /// * `Err(SessionNotReady(state))` - Establishment in progress; caller can retry or show "Establishing…"
3162    pub fn establish_secure_session(&mut self, peer_id: &str) -> Result<Option<WelcomeMessage>> {
3163        if self.is_user_blocked(peer_id) {
3164            return Err(Error::UserBlocked(peer_id.to_string()));
3165        }
3166
3167        let mls = self.mls_manager.clone().ok_or(Error::MlsNotInitialized)?;
3168
3169        // Check if session already exists
3170        let has_session = {
3171            let manager = mls
3172                .read()
3173                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3174            manager.has_session(peer_id)?
3175        };
3176
3177        if has_session {
3178            debug!(peer_id = %peer_id, "Session already exists");
3179            return Ok(None);
3180        }
3181
3182        // Try loading key package from storage (e.g. after restart) before giving up
3183        self.try_load_key_package_from_storage_into_memory(peer_id);
3184
3185        // Check for pending key package (memory, possibly just restored from storage)
3186        // Clone first, only remove after all operations succeed to avoid losing the key package on failure
3187        if let Some(received_pkg) = self.pending_key_packages.get(peer_id).cloned() {
3188            // Check if key package has expired (using local clock)
3189            let now_ms = Utc::now().timestamp_millis() as u64;
3190            if now_ms >= received_pkg.local_expires_at_ms {
3191                warn!(peer_id = %peer_id, "Received key package has expired, discarding");
3192                self.pending_key_packages.remove(peer_id);
3193                self.delete_peer_key_package_from_storage(peer_id);
3194            } else {
3195                self.import_pending_key_package(&mls, peer_id, &received_pkg.key_package_data)?;
3196
3197                // Create session and get welcome message
3198                let welcome = {
3199                    let manager = mls
3200                        .read()
3201                        .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3202                    manager.create_session(peer_id)?
3203                };
3204                // The handshake starts here. This is the auto-establish path
3205                // taken the moment a peer's key package arrives, which is how
3206                // an ordinary session begins and which raises no
3207                // `session_missing` at all; see `emit_mls_session_establishing`.
3208                self.emit_mls_session_establishing(
3209                    peer_id,
3210                    welcome.group_id.as_str(),
3211                    crate::mls_observability::MlsOperationContext::Welcome,
3212                );
3213                // We only ever build a session from a key package the peer
3214                // published, so reaching here proves they run MLS.
3215                self.mark_encryption_capable(peer_id);
3216
3217                // Send welcome message to peer
3218                let welcome_sent = self.send_welcome_message(peer_id, &welcome, false)?;
3219
3220                // All operations succeeded, now safe to remove the key package
3221                self.pending_key_packages.remove(peer_id);
3222                self.delete_peer_key_package_from_storage(peer_id);
3223
3224                let group_id = welcome.group_id.as_str().to_string();
3225                let is_session = group_id.starts_with("session:");
3226                if let Err(err) = self.ensure_session_state_entry(peer_id, "session_created_local")
3227                {
3228                    warn!(
3229                        peer_id = %peer_id,
3230                        error = %err,
3231                        "Failed to persist pending session state"
3232                    );
3233                }
3234
3235                info!(peer_id = %peer_id, group_id = %group_id, "Established secure session");
3236
3237                // Send a fresh key package so the peer has one available for
3238                // group invites (the original was consumed to create this session).
3239                if self.config.encryption.enabled {
3240                    let _ = self.send_key_package_to(peer_id, false);
3241                }
3242
3243                if welcome_sent {
3244                    debug!(peer_id = %peer_id, group_id = %group_id, is_session, "Welcome synchronously sent");
3245                }
3246
3247                return Ok(Some(welcome));
3248            }
3249        }
3250
3251        // No key package available (memory nor storage). Ask the relays for
3252        // one the peer published, then return the non-terminal state so the
3253        // caller can retry; the package that answers establishes the session
3254        // on arrival.
3255        self.request_nostr_key_package_for(peer_id);
3256        Err(Error::SessionNotReady(self.establishment_state(peer_id)?))
3257    }
3258
3259    /// Imports `peer_id`'s pending key package into MLS, reporting a refusal
3260    /// by this device's clock.
3261    ///
3262    /// The one place the engine admits a pending package, so that the refusal
3263    /// is reported whichever route reached it: `establish_secure_session`,
3264    /// taken when a package arrives, and the send path's
3265    /// `ensure_session_establishment`, taken on the first send after a
3266    /// restart or with `auto_key_exchange` off. Reported on only the first,
3267    /// the restart case left a message queued "anyway" with nothing to say why,
3268    /// which is the failure the code exists for. The throttle makes a second
3269    /// route free: one event per peer either way.
3270    ///
3271    /// A window that has not started keeps the package, since it becomes valid
3272    /// once the clocks agree. A window that has closed discards it, exactly as
3273    /// an expired cached package is discarded, because it may be a package a
3274    /// relay held for days rather than a clock, and kept it would fail every
3275    /// attempt until its cached expiry. The next package the peer sends
3276    /// replaces it.
3277    pub(super) fn import_pending_key_package(
3278        &mut self,
3279        mls: &Arc<RwLock<MlsManager>>,
3280        peer_id: &str,
3281        key_package_data: &[u8],
3282    ) -> Result<()> {
3283        let imported = {
3284            let manager = mls
3285                .read()
3286                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3287            manager.import_key_package(peer_id, key_package_data)
3288        };
3289        if let Err(offline_protocol_mls::MlsError::KeyPackageOutsideValidityWindow { expired }) =
3290            &imported
3291        {
3292            let reason = if *expired {
3293                warn!(
3294                    peer_id = %peer_id,
3295                    "Peer key package's validity window has closed by this device's \
3296                     clock; discarding it until the peer sends a fresh one"
3297                );
3298                self.pending_key_packages.remove(peer_id);
3299                self.delete_peer_key_package_from_storage(peer_id);
3300                "The peer's key package has expired by this device's clock. Either \
3301                 this device's date is ahead, or the package is older than its 30-day \
3302                 window; it is discarded and the next one the peer sends replaces it."
3303            } else {
3304                warn!(
3305                    peer_id = %peer_id,
3306                    "Peer key package's validity window has not started by this \
3307                     device's clock; this device cannot start a session with the peer \
3308                     until the clocks agree"
3309                );
3310                "The peer's key package is not valid yet at this device's time. \
3311                 Check this device's date and time, then the peer's."
3312            };
3313            self.warn_control_gate_rejection(
3314                peer_id,
3315                crate::events::SecurityWarningCode::KeyPackageOutsideValidityWindow,
3316                reason,
3317            );
3318        }
3319        Ok(imported?)
3320    }
3321
3322    /// Checks if a pending key package is available for a peer.
3323    ///
3324    /// This can be used to check if session establishment is possible
3325    /// before calling `establish_secure_session`.
3326    ///
3327    /// # Arguments
3328    ///
3329    /// * `peer_id` - The ID of the peer to check
3330    ///
3331    /// # Returns
3332    ///
3333    /// `true` if a key package is available, `false` otherwise
3334    pub fn has_pending_key_package(&self, peer_id: &str) -> bool {
3335        self.pending_key_packages.contains_key(peer_id)
3336    }
3337
3338    /// Returns the current establishment state for a peer.
3339    ///
3340    /// Use this to show "Establishing…" or drive retries without calling send/establish.
3341    pub fn get_establishment_state(&self, peer_id: &str) -> Result<EstablishmentState> {
3342        self.establishment_state(peer_id)
3343    }
3344
3345    /// Whether rich extras (reply context, rich media metadata, forward
3346    /// attribution) on a send toward `peer_id` would travel in the sealed
3347    /// rich body — i.e. the peer's last key package advertised the
3348    /// capability (surviving restarts) and our own `rich_payload_enabled`
3349    /// kill switch is on. When `false` the extras are silently dropped and
3350    /// the message degrades to plain text, so apps can use this to render
3351    /// honest degraded-reply UX instead of discovering the drop after the
3352    /// fact.
3353    pub fn peer_supports_rich_payload(&self, peer_id: &str) -> bool {
3354        self.rich_seal_active(peer_id)
3355    }
3356
3357    /// Creates a session using manually imported key material.
3358    ///
3359    /// This entrypoint is for bindings that expose low-level MLS APIs. It must
3360    /// still keep protocol-level session lifecycle state in sync with MLS state.
3361    pub fn manual_mls_create_session(&mut self, peer_id: &str) -> Result<WelcomeMessage> {
3362        let mls = self.mls_manager.clone().ok_or(Error::MlsNotInitialized)?;
3363        let welcome = {
3364            let manager = mls
3365                .read()
3366                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3367            manager.create_session(peer_id)?
3368        };
3369        self.mark_encryption_capable(peer_id);
3370        if let Err(err) = self.ensure_session_state_entry(peer_id, "manual_session_created") {
3371            warn!(
3372                peer_id = %peer_id,
3373                error = %err,
3374                "Failed to persist pending session state after manual session create"
3375            );
3376        }
3377        Ok(welcome)
3378    }
3379
3380    /// Imports a contact's key package and records them encryption-capable.
3381    ///
3382    /// The entry point for bindings that expose low-level MLS APIs. It exists
3383    /// because the FFI wrapper used to reach straight for the `MlsManager` and
3384    /// pass the since-deleted `KeyPackageTrust::FirstUse` — so an app could
3385    /// import a key package
3386    /// under any peer id with no correlation to that peer, bypassing the check
3387    /// `establish_secure_session` performs. The identity claim no longer needs
3388    /// the detour: `import_key_package` re-derives the address from the leaf
3389    /// signature key and refuses a mismatch on its own. The routing stays
3390    /// because the capability record lives here, not on the manager.
3391    pub fn manual_mls_import_key_package(
3392        &mut self,
3393        peer_id: &str,
3394        key_package_data: &[u8],
3395    ) -> Result<()> {
3396        let mls = self.mls_manager.clone().ok_or(Error::MlsNotInitialized)?;
3397        {
3398            let manager = mls
3399                .read()
3400                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3401            manager.import_key_package(peer_id, key_package_data)?;
3402        }
3403        // A key package we accepted for this peer is proof they run MLS.
3404        self.mark_encryption_capable(peer_id);
3405        Ok(())
3406    }
3407
3408    /// Deletes a 1:1 session and clears protocol-level lifecycle state.
3409    pub fn manual_mls_delete_session(&mut self, peer_id: &str) -> Result<()> {
3410        let mls = self.mls_manager.clone().ok_or(Error::MlsNotInitialized)?;
3411        let manager = mls
3412            .read()
3413            .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3414        manager.delete_session(peer_id)?;
3415
3416        // Apply protocol-state cleanup only after MLS deletion succeeds.
3417        self.confirmed_sessions.remove(peer_id);
3418        self.clear_confirmation_recovery_tracking(peer_id);
3419        self.welcome_lifecycles.remove(peer_id);
3420        // Drop the both-create owner gate too (memory + storage). A leaked gate
3421        // entry would make `can_confirm_from_source` reject every non-decrypt
3422        // confirmation source — including `welcome_received` — on the NEXT
3423        // session with this peer, re-stranding it in Pending (and surviving
3424        // restart via `restore_both_create_awaiting_decrypt`).
3425        self.clear_both_create_awaiting_decrypt(peer_id);
3426        self.clear_session_state_entry(peer_id)?;
3427        self.clear_welcome_lifecycle_entry(peer_id)?;
3428        Ok(())
3429    }
3430
3431    /// Joins a session from a Welcome message and synchronizes confirmation state.
3432    pub fn manual_mls_join_session(
3433        &mut self,
3434        welcome: &WelcomeMessage,
3435    ) -> Result<offline_protocol_mls::GroupInfo> {
3436        let mls = self.mls_manager.clone().ok_or(Error::MlsNotInitialized)?;
3437        let group_info = {
3438            let manager = mls
3439                .read()
3440                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3441            manager.join_session(welcome)?
3442        };
3443        self.handle_manual_welcome_confirmation(&welcome.inviter_id);
3444        Ok(group_info)
3445    }
3446
3447    /// Processes an MLS Welcome message and synchronizes confirmation state.
3448    pub fn manual_mls_process_welcome(
3449        &mut self,
3450        welcome: &WelcomeMessage,
3451    ) -> Result<offline_protocol_mls::GroupInfo> {
3452        let mls = self.mls_manager.clone().ok_or(Error::MlsNotInitialized)?;
3453        let group_info = {
3454            let manager = mls
3455                .read()
3456                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3457            manager.process_welcome(welcome)?
3458        };
3459        self.handle_manual_welcome_confirmation(&welcome.inviter_id);
3460        Ok(group_info)
3461    }
3462
3463    /// Decrypts a user-scoped MLS message and synchronizes confirmation state.
3464    pub fn manual_mls_decrypt_from_user(
3465        &mut self,
3466        encrypted: &EncryptedMessage,
3467    ) -> Result<Option<Vec<u8>>> {
3468        let mls = self.mls_manager.clone().ok_or(Error::MlsNotInitialized)?;
3469        let plaintext = {
3470            let manager = mls
3471                .read()
3472                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3473            manager.decrypt_from_user(encrypted, &encrypted.sender_id)?
3474        };
3475        if plaintext.is_some() {
3476            self.handle_manual_decrypt_confirmation(&encrypted.sender_id);
3477        }
3478        Ok(plaintext)
3479    }
3480
3481    /// Decrypts any MLS message and synchronizes confirmation state for 1:1 flows.
3482    pub fn manual_mls_decrypt(&mut self, encrypted: &EncryptedMessage) -> Result<Option<Vec<u8>>> {
3483        let mls = self.mls_manager.clone().ok_or(Error::MlsNotInitialized)?;
3484        let plaintext = {
3485            let manager = mls
3486                .read()
3487                .map_err(|_| Error::Other("MLS lock poisoned".to_string()))?;
3488            manager.decrypt(encrypted, &encrypted.sender_id)?
3489        };
3490        if plaintext.is_some() && Self::is_session_group_id(encrypted.group_id.as_str()) {
3491            self.handle_manual_decrypt_confirmation(&encrypted.sender_id);
3492        }
3493        Ok(plaintext)
3494    }
3495
3496    pub(crate) fn is_session_group_id(group_id: &str) -> bool {
3497        group_id.starts_with("session:")
3498    }
3499
3500    fn handle_manual_welcome_confirmation(&mut self, peer_id: &str) {
3501        // Shared by `manual_mls_join_session` and `manual_mls_process_welcome`:
3502        // in both cases we just joined a session this peer built for us.
3503        self.mark_encryption_capable(peer_id);
3504        match self.confirm_session_state(peer_id, "welcome_received") {
3505            Ok(true) => {
3506                let _ = self.flush_pending_messages(peer_id);
3507                self.process_pending_decryption(peer_id);
3508            }
3509            Ok(false) => {}
3510            Err(err) => {
3511                warn!(
3512                    peer_id = %peer_id,
3513                    error = %err,
3514                    "Failed to persist session confirmation after manual welcome processing"
3515                );
3516            }
3517        }
3518    }
3519
3520    fn handle_manual_decrypt_confirmation(&mut self, peer_id: &str) {
3521        if !self.can_confirm_from_source(peer_id, "decrypt_success") {
3522            return;
3523        }
3524        match self.confirm_session_state(peer_id, "decrypt_success") {
3525            Ok(true) => {
3526                let _ = self.flush_pending_messages(peer_id);
3527            }
3528            Ok(false) => {}
3529            Err(err) => {
3530                warn!(
3531                    peer_id = %peer_id,
3532                    error = %err,
3533                    "Failed to persist session confirmation after manual decrypt"
3534                );
3535            }
3536        }
3537    }
3538
3539    /// Processes internal MLS protocol messages.
3540    ///
3541    /// Returns `Some(InternalMessageResult::Consumed)` if the message was an internal
3542    /// protocol message that should not be surfaced to the application.
3543    /// Returns `Some(InternalMessageResult::Decrypted(plaintext))` if the message was
3544    /// encrypted and successfully decrypted.
3545    /// Returns `None` if the message is not an internal message.
3546    pub(crate) fn process_internal_message(
3547        &mut self,
3548        message: &Message,
3549    ) -> Option<InternalMessageResult> {
3550        self.process_internal_message_via(message, None)
3551    }
3552
3553    /// [`Self::process_internal_message`] with the transport the frame
3554    /// arrived on, when the caller knows it. Handlers that trust relay-server
3555    /// answers (`__GROUP_CREATED__` setting `relay_synced`) require the
3556    /// Internet transport; with `None` they treat the frame as untrusted.
3557    pub(crate) fn process_internal_message_via(
3558        &mut self,
3559        message: &Message,
3560        arrival_transport: Option<TransportType>,
3561    ) -> Option<InternalMessageResult> {
3562        let content = &message.content;
3563
3564        // Run the security gate for control messages (transport identity +
3565        // signature verification + freshness). `Rejected` drops the message;
3566        // `Proceed` carries whether the frame was actually signed, for the one
3567        // handler that consumes a payload field as authenticated data, and
3568        // whether its age was established, for the one that acts on a
3569        // directive worth replaying.
3570        let (signed, freshness_bound) =
3571            match self.security_gate_control_message(message, arrival_transport) {
3572                ControlGateOutcome::Rejected(result) => return Some(result),
3573                ControlGateOutcome::Proceed {
3574                    signed,
3575                    freshness_bound,
3576                } => (signed, freshness_bound),
3577            };
3578
3579        let sender = message.sender.as_str();
3580
3581        // Handle key package messages
3582        if let Some(data) = content.strip_prefix(internal_prefixes::KEY_PACKAGE) {
3583            // The stamp travels only when it was signed *and* checked, so a
3584            // handler cannot reach for it in the case where it means nothing.
3585            let freshness = freshness_bound.then(|| message.timestamp.as_millis());
3586            self.handle_key_package_message(sender, data, signed, freshness);
3587            return Some(InternalMessageResult::Consumed);
3588        }
3589
3590        if content.starts_with(internal_prefixes::SESSION_CONFIRM_PROBE) {
3591            self.handle_session_confirm_probe(sender, content);
3592            return Some(InternalMessageResult::Consumed);
3593        }
3594
3595        if content.starts_with(internal_prefixes::SESSION_CONFIRM_ACK) {
3596            self.handle_session_confirm_ack(sender, content);
3597            return Some(InternalMessageResult::Consumed);
3598        }
3599
3600        // Handle welcome messages (session invitation)
3601        if let Some(data) = content.strip_prefix(internal_prefixes::WELCOME) {
3602            let freshness = freshness_bound.then(|| message.timestamp.as_millis());
3603            self.handle_welcome_message(sender, data, freshness);
3604            return Some(InternalMessageResult::Consumed);
3605        }
3606
3607        // Handle encrypted messages
3608        if let Some(data) = content.strip_prefix(internal_prefixes::ENCRYPTED) {
3609            if let Some(result) =
3610                self.handle_encrypted_message(sender, data, message, arrival_transport)
3611            {
3612                return Some(result);
3613            }
3614            return Some(InternalMessageResult::Consumed);
3615        }
3616
3617        // Handle connection request messages
3618        if let Some(data) = content.strip_prefix(internal_prefixes::CONN_REQUEST) {
3619            self.handle_connection_request(sender, data);
3620            return Some(InternalMessageResult::Consumed);
3621        }
3622
3623        // Handle connection accepted messages
3624        if let Some(data) = content.strip_prefix(internal_prefixes::CONN_ACCEPT) {
3625            self.handle_connection_accepted(sender, data);
3626            return Some(InternalMessageResult::Consumed);
3627        }
3628
3629        // Handle connection rejected messages
3630        if content
3631            .strip_prefix(internal_prefixes::CONN_REJECT)
3632            .is_some()
3633        {
3634            self.handle_connection_rejected(sender);
3635            return Some(InternalMessageResult::Consumed);
3636        }
3637
3638        // Handle connection cancelled messages
3639        if content
3640            .strip_prefix(internal_prefixes::CONN_CANCEL)
3641            .is_some()
3642        {
3643            self.handle_connection_cancelled(sender);
3644            return Some(InternalMessageResult::Consumed);
3645        }
3646
3647        // --- Presence, typing, and read receipt messages ---
3648
3649        if let Some(data) = content.strip_prefix(internal_prefixes::PRESENCE) {
3650            self.handle_presence_message(sender, data);
3651            return Some(InternalMessageResult::Consumed);
3652        }
3653
3654        if let Some(data) = content.strip_prefix(internal_prefixes::TYPING_INDICATOR) {
3655            self.handle_typing_indicator(sender, data);
3656            return Some(InternalMessageResult::Consumed);
3657        }
3658
3659        if let Some(data) = content.strip_prefix(internal_prefixes::READ_RECEIPT) {
3660            self.handle_read_receipt(sender, data);
3661            return Some(InternalMessageResult::Consumed);
3662        }
3663
3664        // A custody receipt from a neighbour holding one of our frames. Past
3665        // the control gate like every signed frame; it suppresses re-deposit
3666        // toward that custodian and settles nothing (`docs/spec/custody.md`).
3667        // Consumed whatever the body says: a malformed one is refused
3668        // silently, and a receipt never requests an acknowledgement.
3669        if let Some(data) = content.strip_prefix(internal_prefixes::CUSTODY_RECEIPT) {
3670            self.handle_custody_receipt(sender, data, message.timestamp.as_millis());
3671            return Some(InternalMessageResult::Consumed);
3672        }
3673
3674        // --- Group (mesh/MLS) messages ---
3675
3676        if let Some(data) = content.strip_prefix(internal_prefixes::GROUP_MLS_MSG) {
3677            return Some(self.handle_group_mls_msg_via(message, sender, data, arrival_transport));
3678        }
3679
3680        if let Some(data) = content.strip_prefix(internal_prefixes::GROUP_MLS_WELCOME) {
3681            let mid = message.id.as_str();
3682            self.handle_group_mls_welcome(&mid, sender, data);
3683            return Some(InternalMessageResult::Consumed);
3684        }
3685
3686        if let Some(data) = content.strip_prefix(internal_prefixes::GROUP_MLS_COMMIT) {
3687            let mid = message.id.as_str();
3688            self.handle_group_mls_commit(&mid, sender, data);
3689            return Some(InternalMessageResult::Consumed);
3690        }
3691
3692        if let Some(data) = content.strip_prefix(internal_prefixes::GROUP_MLS_LEAVE) {
3693            let mid = message.id.as_str();
3694            self.handle_group_mls_leave(&mid, sender, data);
3695            return Some(InternalMessageResult::Consumed);
3696        }
3697
3698        if let Some(data) = content.strip_prefix(internal_prefixes::GROUP_ROLE_CHANGE) {
3699            let mid = message.id.as_str();
3700            self.handle_group_role_change(&mid, sender, data);
3701            return Some(InternalMessageResult::Consumed);
3702        }
3703
3704        if let Some(data) = content.strip_prefix(internal_prefixes::GROUP_RENAME) {
3705            let mid = message.id.as_str();
3706            self.handle_group_rename(&mid, sender, data);
3707            return Some(InternalMessageResult::Consumed);
3708        }
3709
3710        // --- Group (relay) and service messages ---
3711
3712        if content.starts_with(internal_prefixes::GROUP_CREATED)
3713            || content.starts_with(internal_prefixes::GROUP_MSG)
3714            || content.starts_with(internal_prefixes::GROUP_MEMBER_ADDED)
3715            || content.starts_with(internal_prefixes::GROUP_MEMBER_REMOVED)
3716            || content.starts_with(internal_prefixes::GROUP_INFO)
3717            || content.starts_with(internal_prefixes::USER_GROUPS)
3718            || content.starts_with(internal_prefixes::GROUP_ERROR)
3719        {
3720            self.handle_group_relay_message(sender, content, arrival_transport);
3721            return Some(InternalMessageResult::Consumed);
3722        }
3723
3724        // --- Service discovery & request/response ---
3725        if content.starts_with(offline_protocol_services::SVC_MESSAGE_PREFIX) {
3726            self.handle_service_message(sender, content, message, false);
3727            return Some(InternalMessageResult::Consumed);
3728        }
3729
3730        None // Not an internal message
3731    }
3732
3733    /// Registers an event handler.
3734    ///
3735    /// # Arguments
3736    ///
3737    /// * `handler` - Callback function that will be called for each event
3738    pub fn on_event<F>(&mut self, handler: F)
3739    where
3740        F: Fn(Event) + Send + Sync + 'static,
3741    {
3742        let Ok(mut state) = lock_shared_state(&self.shared_state) else {
3743            error!("Failed to lock shared state in on_event");
3744            return;
3745        };
3746        state.event_handlers.push(Arc::new(handler));
3747    }
3748
3749    /// Processes pending operations (retries, timeouts, etc.).
3750    ///
3751    /// This should be called periodically to handle background tasks.
3752    pub fn process(&mut self) -> Result<()> {
3753        {
3754            let state = lock_shared_state(&self.shared_state)?;
3755            if state.state != ProtocolState::Running {
3756                return Ok(()); // Don't process if not running
3757            }
3758        }
3759
3760        // Transmit forwards whose hold has elapsed. Runs early and before the
3761        // retry ladders: other people's traffic is time-sensitive in a way our
3762        // own retries are not — a frame held too long is dropped rather than
3763        // sent late.
3764        self.flush_mesh_relays();
3765        // Held frames past their hold, and receipt suppressions past their
3766        // end. Throttled inside; hours-long holds need no per-tick walk.
3767        self.sweep_custody();
3768
3769        // One step's failure never starves another. Every fallible step runs
3770        // on every tick and the first error is reported once they all have.
3771        // The steps are coupled: the retry queue fills the pending-ACK table
3772        // and the timeout sweep is what empties it, so a retry error that
3773        // returned early used to stop the sweep for good.
3774        let mut first_error: Option<crate::Error> = None;
3775        let mut note = |step: &'static str, result: Result<()>| {
3776            if let Err(err) = result {
3777                warn!(step, error = %err, "process() step failed; continuing the tick");
3778                first_error.get_or_insert(err);
3779            }
3780        };
3781        // Before the retry ladder, so a frame the app sent first is not
3782        // overtaken by one that only reached the retry queue later.
3783        self.flush_peer_windows();
3784        self.flush_due_coalesced_acks();
3785        note("retry_queue", self.process_retry_queue());
3786        note("welcome_retry_queue", self.process_welcome_retry_queue());
3787        self.process_relay_register_retries();
3788        self.process_relay_broadcast_report_timeouts();
3789        note("timed_out_acks", self.process_timed_out_acks());
3790
3791        // Throttle session reconciliation to avoid expensive storage I/O
3792        // (list_sessions → Keychain/Keystore) on every tick. Only run when
3793        // there's pending work AND enough time has elapsed since the last run.
3794        self.run_throttled_reconciliation("process_tick");
3795
3796        let _ = self.prune_expired_pending_global_front(Instant::now(), 256);
3797        // Batched like the Lamport clock: the seen set reaches disk on a
3798        // change threshold or a time cadence, never per message.
3799        self.persist_dedup_seen_if_due();
3800        self.pump_media_transfers();
3801        #[cfg(feature = "data")]
3802        self.send_due_data_settle_offers(Instant::now());
3803        self.refresh_nostr_key_package_slots();
3804        self.refresh_nostr_discovery_claim();
3805        // A relay is free never to send end-of-stored-events, and that is the
3806        // only natural completion signal a resolution has. Without this sweep
3807        // a quiet relay turns a lookup into a hang rather than an empty answer.
3808        self.sweep_username_resolutions();
3809        self.cleanup_expired_entries();
3810        self.evaluate_relay_role();
3811        self.tick_telemetry_categories();
3812
3813        match first_error {
3814            Some(err) => Err(err),
3815            None => Ok(()),
3816        }
3817    }
3818
3819    /// Reports this device's condition to the forwarding governor and announces
3820    /// any change in whether it is acting as a relay (`RelayPromoted`,
3821    /// `RelayDemoted`, `RelayDemotedBattery`).
3822    ///
3823    /// Runs every tick independently of telemetry: these are app-facing events
3824    /// delivered through the `EventCallback` channel, which is live whether or
3825    /// not a telemetry sink is installed. It also keeps the
3826    /// `DeviceCapabilitySnapshot.relay_role` signal honest, since
3827    /// `tick_telemetry_categories` reads the standing immediately after.
3828    ///
3829    /// **The relay standing is observed, not predicted.** It says this device
3830    /// has been carrying other people's traffic, not that its battery and
3831    /// neighbor count suggest it could — those only bias *how much* it carries
3832    /// (`set_conditions`), continuously and with no threshold to cross. So the
3833    /// promotion here is a report about the mesh, and an app can trust
3834    /// `is_relay()` to mean the device is doing the work.
3835    ///
3836    /// Two things still transition on policy rather than on observation, and
3837    /// both are gates rather than predictions: relaying switched off in
3838    /// configuration, and the battery falling through the forwarding floor.
3839    /// Each stops forwarding outright, so the standing is surrendered at once
3840    /// instead of decaying over the activity windows — the window measures
3841    /// quiet, and this is refusal.
3842    ///
3843    /// Unlike the threshold policy this replaces, a device with no battery
3844    /// reading still participates: forwarding is what is being observed, and
3845    /// it happens whether or not the host has reported a level.
3846    fn evaluate_relay_role(&mut self) {
3847        let (_statuses, available) = self.transport_manager.snapshot_status_and_available();
3848        let (battery_level, is_charging) =
3849            device_battery_from_available(self.transport_manager.current_transport(), &available);
3850
3851        let eager = matches!(self.config.relay.relay_priority, RelayPriority::Always);
3852        // The bias ramp is measured from the level at which forwarding actually
3853        // stops, which is the configured minimum raised to the hard critical
3854        // floor — not the raw configured value, which may be set below it.
3855        // Passing the unclamped one would start the ramp under the gate and
3856        // scale a device that is about to stop forwarding altogether.
3857        let forwarding_floor = self.relay_battery_floor(is_charging);
3858        self.mesh_relay
3859            .set_conditions(battery_level, is_charging, eager, forwarding_floor);
3860        let relay = &self.config.relay;
3861
3862        // Relaying switched off in configuration.
3863        if !relay.allow_relay || matches!(relay.relay_priority, RelayPriority::Never) {
3864            if self.mesh_relay.force_inactive() {
3865                self.emit_event(Event::relay_demoted("relaying disabled by configuration"));
3866            }
3867            return;
3868        }
3869
3870        // Battery through the forwarding floor. Same gate `try_relay_message`
3871        // applies to each frame, evaluated from the reading already in hand.
3872        if !self.battery_allows_relaying_with(battery_level, is_charging) {
3873            if self.mesh_relay.force_inactive() {
3874                self.emit_event(Event::relay_demoted_battery(
3875                    battery_level.unwrap_or(0),
3876                    self.relay_battery_floor(is_charging),
3877                ));
3878            }
3879            return;
3880        }
3881
3882        match self.mesh_relay.observe_activity(Instant::now()) {
3883            Some(RelayActivity::Began { .. }) => {
3884                let connection_count = self.known_peers.len();
3885                self.emit_event(Event::relay_promoted(connection_count, battery_level));
3886            }
3887            Some(RelayActivity::Ceased) => {
3888                self.emit_event(Event::relay_demoted(
3889                    "no traffic carried for other devices recently",
3890                ));
3891            }
3892            None => {}
3893        }
3894    }
3895
3896    /// The relay standing this device currently holds, for telemetry.
3897    fn relay_role(&self) -> RelayRole {
3898        if self.mesh_relay.is_active_relay() {
3899            RelayRole::Relay
3900        } else {
3901            RelayRole::Regular
3902        }
3903    }
3904
3905    /// Per-tick telemetry work: diff transport statuses, diff device
3906    /// capability, and emit a `MetricsFrame` when `metrics_cadence` has
3907    /// elapsed. No-ops unless a sink has been installed.
3908    ///
3909    /// Runs at the end of `process()` so reliability/queue state observed
3910    /// here is the same state the rest of the tick has already advanced.
3911    /// Snapshots `get_available_transports()` exactly once per tick and
3912    /// reuses the map across every downstream helper that needs it.
3913    ///
3914    /// If an earlier step in `process()` returns `Err(...)`, this method is
3915    /// skipped and telemetry emission for that interval is deferred to the
3916    /// next successful tick — the cadence guarantee relaxes under sustained
3917    /// partial failure.
3918    ///
3919    /// # Sink-panic semantics
3920    ///
3921    /// Each per-tick cursor is advanced *before* the emit call that
3922    /// observes it, so a panicking sink does not cause re-delivery of the
3923    /// record it panicked on. The transport-status path advances
3924    /// **per-transport**: when multiple transitions fall in a single tick,
3925    /// a panic on transition K commits K's entry (at-most-once for K) but
3926    /// leaves entries L..N in `transport_status_snapshot` at their previous
3927    /// values, so the next tick re-diffs and emits them fresh. This avoids
3928    /// silent data loss for transitions that never reached the sink.
3929    ///
3930    /// The device-capability and metrics-frame paths emit at most one
3931    /// record per tick each, so a simpler "advance then emit" discipline
3932    /// suffices there: a panic advances the cursor and the same record is
3933    /// not re-emitted on the next tick.
3934    fn tick_telemetry_categories(&mut self) {
3935        let Some(ctx) = self.telemetry.clone().filter(|ctx| ctx.enabled()) else {
3936            return;
3937        };
3938        let now_ms = Utc::now().timestamp_millis();
3939        let now = Instant::now();
3940
3941        // One lock-per-transport pass: get statuses (for the transition
3942        // diff) and the available-only metrics map (for the metrics frame
3943        // and the device-capability diff). Reused across every helper
3944        // below.
3945        let (current_statuses, available) = self.transport_manager.snapshot_status_and_available();
3946
3947        // Transport-status diff: one record per changed transport. Commit
3948        // each transport's new snapshot entry *before* emitting its event
3949        // so a panic on event K is at-most-once for K; untouched entries
3950        // L..N stay at their previous values and get re-diffed next tick.
3951        let transitions =
3952            diff_transport_state(now_ms, &self.transport_status_snapshot, &current_statuses);
3953        for event in transitions {
3954            let transport = event.transport;
3955            match current_statuses.get(&transport) {
3956                Some(status) => {
3957                    self.transport_status_snapshot.insert(transport, *status);
3958                }
3959                None => {
3960                    self.transport_status_snapshot.remove(&transport);
3961                }
3962            }
3963            dispatch_record(&ctx.sink, &TelemetryRecord::TransportState(event));
3964            // An internet-capable transport coming up is the pipe's cue to
3965            // send what an offline stretch queued; the pipe decides which
3966            // transports count.
3967            #[cfg(feature = "telemetry-pipe")]
3968            if let Some(pipe) = &self.telemetry_pipe {
3969                pipe.notify_transport(event.transport, event.current);
3970            }
3971        }
3972
3973        // Device capability diff. At-most-one emission per tick, so the
3974        // simple advance-before-emit pattern preserves at-most-once.
3975        let (battery_level, is_charging) =
3976            device_battery_from_available(self.transport_manager.current_transport(), &available);
3977        let relay_role = self.relay_role();
3978        let device_now = DeviceSnap::from_parts(battery_level, is_charging, relay_role);
3979        let device_change =
3980            diff_device_capability(now_ms, self.device_capability_snapshot, device_now);
3981        self.device_capability_snapshot = Some(device_now);
3982        if let Some(snapshot) = device_change {
3983            dispatch_record(&ctx.sink, &TelemetryRecord::Device(snapshot));
3984        }
3985
3986        // Periodic metrics snapshot. Rearm cadence before emit so a
3987        // panicking sink doesn't cause an immediate retry next tick.
3988        if let Some(cadence) = ctx.config.metrics_cadence() {
3989            let due = match self.last_metrics_emit_at {
3990                None => true,
3991                Some(prev) => now.saturating_duration_since(prev) >= cadence,
3992            };
3993            if due {
3994                let is_local_relay = matches!(relay_role, RelayRole::Relay);
3995                let frame = build_metrics_frame(
3996                    now_ms,
3997                    self.transport_manager.current_transport(),
3998                    &available,
3999                    &self.retry_queue,
4000                    &self.deduplicator,
4001                    &self.ack_manager,
4002                    self.known_peers.len(),
4003                    is_local_relay,
4004                );
4005                self.last_metrics_emit_at = Some(now);
4006                dispatch_record(
4007                    &ctx.sink,
4008                    &TelemetryRecord::MetricsSnapshot(Box::new(frame)),
4009                );
4010            }
4011        }
4012    }
4013
4014    /// Processes messages ready for retry from the retry queue.
4015    ///
4016    /// EDGE CASE HANDLING:
4017    /// - Checks transport availability before each retry attempt
4018    /// - Handles transport switch mid-retry
4019    /// - Properly tracks retry counts and transport failures
4020    fn process_retry_queue(&mut self) -> Result<()> {
4021        // Limit batch size to prevent blocking on large queues
4022        let mut max_batch_size = crate::constants::FLUSH_BATCH_LIMIT;
4023
4024        // Opt-in resend rate cap: bound how fast the RETRY/PROBE path re-sends,
4025        // so a backlog of resends to unreachable peers cannot burst past the
4026        // relay's per-connection rate limit and flap the connection. Refills a
4027        // token bucket by elapsed wall-time and caps this call's batch to the
4028        // whole tokens available. Only resends are bounded; first-attempt sends,
4029        // ACKs, control frames and handshakes take other paths and are never
4030        // throttled, so live traffic keeps full throughput and latency. Fully
4031        // off by default (unchanged retry behaviour for every existing app).
4032        if self.config.reliability.retry.edge_driven_unreachable_dm {
4033            let now = Instant::now();
4034            match self.retry_drain_last {
4035                Some(last) => {
4036                    let elapsed = now.duration_since(last).as_secs_f64();
4037                    self.retry_drain_tokens = (self.retry_drain_tokens
4038                        + elapsed * crate::protocol::types::RETRY_DRAIN_RATE_PER_SEC)
4039                        .min(crate::protocol::types::RETRY_DRAIN_BURST);
4040                }
4041                None => {
4042                    // First drain: start with a full burst budget.
4043                    self.retry_drain_tokens = crate::protocol::types::RETRY_DRAIN_BURST;
4044                }
4045            }
4046            self.retry_drain_last = Some(now);
4047            let budget = self.retry_drain_tokens.floor() as usize;
4048            max_batch_size = max_batch_size.min(budget);
4049        }
4050
4051        let mut processed = 0;
4052
4053        while processed < max_batch_size {
4054            let mut entry = match self.retry_queue.dequeue_ready() {
4055                Some(e) => e,
4056                None => break,
4057            };
4058
4059            processed += 1;
4060
4061            // No slot to track this resend: put it back unchanged and try
4062            // again shortly. Checked before the re-seal, so a held resend
4063            // burns no ratchet generation, and before the send, so nothing
4064            // goes out that cannot be tracked. The retry count is not
4065            // advanced: being held is not an attempt, so the wait is the
4066            // ladder's first rung rather than this entry's next one.
4067            if entry.message.requires_ack && !self.ack_manager.has_capacity_for(&entry.message.id) {
4068                let recheck_ms = self.config.reliability.retry.initial_delay_ms;
4069                let _ = self.retry_queue.enqueue_with_delay(
4070                    entry.message,
4071                    entry.retry_count,
4072                    recheck_ms,
4073                );
4074                continue;
4075            }
4076
4077            // A frame that has not gone out yet waits for the peer's window
4078            // instead, out of this queue: re-checking it every second would
4079            // spend the batch on frames that cannot move.
4080            if !self.peer_window_admits(&entry.message) {
4081                self.hold_for_peer_window(&entry.message);
4082                continue;
4083            }
4084
4085            let previous_transport = self.transport_manager.current_transport();
4086            // Normally finds the entry already there: a retry entry outlives
4087            // its outbox entry only if a give-up path skipped
4088            // `retire_undeliverable_message`, and at capacity this call would
4089            // then evict a live message to bring a settled one back.
4090            self.ensure_outbox_entry(&entry.message);
4091
4092            // Tier 2: re-seal against the peer's current session before this
4093            // resend, so a post-re-key epoch change no longer leaves the bytes
4094            // undecryptable. No-op for media/plaintext/unconfirmed sessions.
4095            self.reseal_for_resend_in_place(&mut entry.message);
4096
4097            let forced_transport = self.pinned_media_transport_for_message(&entry.message.id);
4098            let send_result = if let Some(transport) = forced_transport {
4099                self.transport_manager
4100                    .send_via_transport(&entry.message, transport)
4101            } else {
4102                self.transport_manager.send(&entry.message)
4103            };
4104            let current_transport =
4105                forced_transport.or_else(|| self.transport_manager.current_transport());
4106
4107            match send_result {
4108                Ok(()) => {
4109                    // Never `?`. This propagated once, and `process()` then
4110                    // returned before `process_timed_out_acks` on every tick:
4111                    // the step that frees slots never ran again, so the table
4112                    // stayed full and the engine never recovered. The capacity
4113                    // check above makes a failure here a priority-eviction
4114                    // race at most.
4115                    let ack_registered_now = match self.ensure_ack_registration(&entry.message) {
4116                        Ok(registered) => registered,
4117                        Err(err) => {
4118                            warn!(
4119                                message_id = %entry.message.id,
4120                                error = %err,
4121                                "ACK registration failed after a retry send"
4122                            );
4123                            false
4124                        }
4125                    };
4126
4127                    if ack_registered_now {
4128                        // The resend registered a brand-new ACK (the prior one
4129                        // was cleared, e.g. by an ACK timeout that re-queued
4130                        // this message). A fresh ACK starts at retry_count 0, so
4131                        // carry the count the retry-queue entry already holds —
4132                        // advanced by this attempt — onto it. Without this the
4133                        // backoff ladder resets to 0 on every relay-accepted
4134                        // resend and `delay_for_retry` is pinned at its 1s floor
4135                        // forever for a peer that never ACKs (an offline
4136                        // recipient), flooding the relay ~once per second.
4137                        self.ack_manager.set_retry_count(
4138                            &entry.message.id,
4139                            entry.retry_count.saturating_add(1),
4140                        );
4141                    } else {
4142                        self.ack_manager.increment_retry_count(&entry.message.id);
4143                    }
4144                    self.mark_message_sent(
4145                        &entry.message,
4146                        current_transport,
4147                        Some(entry.retry_count.saturating_add(1)),
4148                    );
4149
4150                    if let Some(transport) = current_transport {
4151                        self.transport_manager.reset_retry_count(transport);
4152                    }
4153                    self.offer_resend_to_mesh(&entry.message, true);
4154
4155                    debug!(
4156                        message_id = %entry.message.id,
4157                        retry_count = entry.retry_count,
4158                        transport = ?current_transport,
4159                        "Retry send succeeded"
4160                    );
4161                }
4162                Err(e) => {
4163                    self.offer_resend_to_mesh(&entry.message, false);
4164                    // Re-enqueue with incremented retry count for backoff
4165                    let next_retry_at = self
4166                        .retry_queue
4167                        .enqueue(entry.message.clone(), entry.retry_count + 1);
4168
4169                    if let Some(transport) = forced_transport.or(previous_transport) {
4170                        self.transport_manager.record_retry_failure(transport);
4171                    }
4172
4173                    // Surface the schedule so apps can show retry state
4174                    // instead of inferring it from silence. None = the id
4175                    // was already queued; the earlier emission stands.
4176                    if let Some(retry_at) = next_retry_at {
4177                        self.emit_event(Event::message_retrying(
4178                            entry.message.id.clone(),
4179                            entry.message.recipient.as_str().to_string(),
4180                            entry.retry_count + 1,
4181                            retry_at.timestamp_millis(),
4182                        ));
4183                    }
4184
4185                    debug!(
4186                        message_id = %entry.message.id,
4187                        retry_count = entry.retry_count,
4188                        transport = ?current_transport,
4189                        error = %e,
4190                        "Retry send failed, will retry later"
4191                    );
4192                }
4193            }
4194        }
4195
4196        if processed > 0 {
4197            debug!(processed = processed, "Processed retry queue entries");
4198            // Spend the resend-rate tokens actually used (opt-in cap only).
4199            if self.config.reliability.retry.edge_driven_unreachable_dm {
4200                self.retry_drain_tokens = (self.retry_drain_tokens - processed as f64).max(0.0);
4201            }
4202        }
4203
4204        Ok(())
4205    }
4206
4207    /// Processes timed out ACKs and handles retry or failure.
4208    fn process_timed_out_acks(&mut self) -> Result<()> {
4209        let timed_out = self.ack_manager.drain_timed_out();
4210        if timed_out.is_empty() {
4211            return Ok(());
4212        }
4213        // A frame still waiting in a local queue has not been answered because
4214        // it has not been sent. Retrying it would queue a second, re-sealed
4215        // copy behind the first: on a congested link that is what grew the
4216        // queue, the round trip and the receiver's ratchet gap together in the
4217        // 2026-10-19 volume test. It gets a bounded extension instead.
4218        let queued = self.transport_manager.queued_message_ids();
4219        for pending in timed_out {
4220            let message_id = pending.message_id.clone();
4221
4222            if queued.contains(&message_id.as_str())
4223                && self
4224                    .ack_manager
4225                    .extend_while_queued(&message_id, QUEUED_ACK_GRACE_MAX)
4226            {
4227                continue;
4228            }
4229
4230            if pending.retry_count >= self.config.reliability.retry.max_retries {
4231                self.handle_max_retries_exceeded(&message_id, pending.retry_count)?;
4232                continue;
4233            }
4234
4235            self.handle_ack_timeout_retry(&message_id, pending.retry_count)?;
4236        }
4237        Ok(())
4238    }
4239
4240    /// Handles a message that has exceeded maximum retries.
4241    fn handle_max_retries_exceeded(
4242        &mut self,
4243        message_id: &MessageId,
4244        retry_count: u32,
4245    ) -> Result<()> {
4246        // A plain DM whose recipient still holds a live unreachable-park
4247        // counter re-parks instead of settling: the exhausted budget was burnt
4248        // by reachability probes — sends that succeed locally without proving
4249        // the peer is back — not by a peer believed reachable (see
4250        // try_repark_exhausted_dm).
4251        if self.try_repark_exhausted_dm(message_id) {
4252            return Ok(());
4253        }
4254
4255        // Retry exhaustion is terminal for a connection request: settle the
4256        // pending entry and surface the typed undeliverable signal alongside
4257        // the generic message_failed, so apps keep connection-request
4258        // context without parsing reason strings.
4259        let undeliverable_recipient = self.take_undeliverable_connection_request(message_id);
4260        // Read before `retire_undeliverable_message` below removes the entry.
4261        let app_send = self.is_app_send(message_id);
4262
4263        let state = lock_shared_state(&self.shared_state).map_err(|e| {
4264            error!(
4265                "Failed to lock shared state for message failed event: {}",
4266                e
4267            );
4268            e
4269        })?;
4270        let failed = Event::message_failed(message_id.clone(), "Max retries exceeded", retry_count);
4271        // Telemetry counts the app's own messages only; see `emit_settlement`.
4272        state.emit_settlement(failed, app_send);
4273        if let Some(recipient) = undeliverable_recipient {
4274            warn!(
4275                recipient = %recipient,
4276                message_id = %message_id,
4277                "Connection request undeliverable: max retries exceeded"
4278            );
4279            state.emit_event(Event::connection_request_undeliverable(
4280                recipient,
4281                message_id.as_str(),
4282                "max_retries_exceeded",
4283            ));
4284        }
4285        drop(state);
4286
4287        self.retire_undeliverable_message(message_id, "max retries exceeded");
4288        Ok(())
4289    }
4290
4291    /// Handles retry logic for a timed out ACK.
4292    fn handle_ack_timeout_retry(&mut self, message_id: &MessageId, retry_count: u32) -> Result<()> {
4293        if let Some(entry) = self
4294            .outbox
4295            .get(message_id)
4296            .or_else(|| self.media_outbox.get(message_id))
4297        {
4298            let message_clone = entry.message.clone();
4299            let recipient = entry.message.recipient.as_str().to_string();
4300            let last_transport = entry.last_transport;
4301
4302            // enqueue is infallible (retry queue has no attempt limit)
4303            let next_retry_at = self.retry_queue.enqueue(message_clone, retry_count);
4304            if let Some(transport) = last_transport {
4305                self.transport_manager.record_retry_failure(transport);
4306            }
4307            if let Some(retry_at) = next_retry_at {
4308                self.emit_event(Event::message_retrying(
4309                    message_id.clone(),
4310                    recipient,
4311                    retry_count,
4312                    retry_at.timestamp_millis(),
4313                ));
4314            }
4315        } else {
4316            self.handle_missing_outbox_entry(message_id, retry_count)?;
4317        }
4318        Ok(())
4319    }
4320
4321    /// Emits a protocol event to all registered handlers.
4322    ///
4323    /// Silently no-ops if the shared state lock is poisoned.
4324    pub(crate) fn emit_event(&self, event: Event) {
4325        if let Ok(state) = lock_shared_state(&self.shared_state) {
4326            state.emit_event(event);
4327        }
4328    }
4329
4330    /// Emits a message's terminal event (`MessageDelivered` or
4331    /// `MessageFailed`) to the app, and to telemetry only when `telemetry` is
4332    /// set.
4333    ///
4334    /// Telemetry's delivered and failed counts are read against its sent
4335    /// count, which only the app send path reports, so all three count the
4336    /// same messages: the app's own. A settlement for a media chunk, an
4337    /// internal frame, or a message whose origin is no longer known still
4338    /// reaches `on_event` and stays out of telemetry. See
4339    /// [`OutboxEntry::app_send`].
4340    pub(crate) fn emit_settlement(&self, event: Event, telemetry: bool) {
4341        if let Ok(state) = lock_shared_state(&self.shared_state) {
4342            state.emit_settlement(event, telemetry);
4343        }
4344    }
4345
4346    /// Whether `message_id` is an outbox entry the app send path created.
4347    pub(crate) fn is_app_send(&self, message_id: &MessageId) -> bool {
4348        self.outbox
4349            .get(message_id)
4350            .is_some_and(|entry| entry.app_send)
4351    }
4352
4353    /// Emits a terminal settlement, or parks it until `start()` if the event
4354    /// pipeline is not live yet.
4355    ///
4356    /// Restore paths settle messages the app is still holding ids for. They run
4357    /// from `initialize_mls`, before `start()` and often before the app has
4358    /// installed its event callback, so emitting directly there would silently
4359    /// discard exactly the signals that exist to stop an id from hanging
4360    /// forever. Once running, this is a plain emit — the same call is used from
4361    /// `process()`-driven expiry, where no deferral is wanted.
4362    pub(crate) fn settle_restored_message_failure(&mut self, event: Event) {
4363        let running = self.protocol_is_running();
4364        self.settle_one_restored_message_failure(event, true, running);
4365    }
4366
4367    /// [`Self::settle_restored_message_failures`] for restored outbox entries,
4368    /// each paired with its [`OutboxEntry::app_send`]: only the app's own
4369    /// reach telemetry, as on the live path.
4370    pub(crate) fn settle_restored_outbox_failures(
4371        &mut self,
4372        settlements: impl IntoIterator<Item = (Event, bool)>,
4373    ) {
4374        let running = self.protocol_is_running();
4375        for (event, telemetry) in settlements {
4376            self.settle_one_restored_message_failure(event, telemetry, running);
4377        }
4378    }
4379
4380    /// Settles a batch, taking the shared-state lock once rather than once per
4381    /// event. Restore emits these in loops that a tampered or pre-split store
4382    /// can drive into the thousands.
4383    pub(crate) fn settle_restored_message_failures(
4384        &mut self,
4385        events: impl IntoIterator<Item = Event>,
4386    ) {
4387        let running = self.protocol_is_running();
4388        for event in events {
4389            self.settle_one_restored_message_failure(event, true, running);
4390        }
4391    }
4392
4393    /// Whether [`Self::start`] has run and nothing has stopped it since.
4394    ///
4395    /// Two callers want different things from the same fact. The restore
4396    /// paths above want to know whether an emit reaches an application at
4397    /// all. The document layer wants to know whether a frame it is about to
4398    /// build can leave the device, because a send attempted before the
4399    /// transports are up stamps a rate-limit window against an offer that
4400    /// was never carried.
4401    pub(crate) fn protocol_is_running(&self) -> bool {
4402        lock_shared_state(&self.shared_state)
4403            .map(|state| state.state == ProtocolState::Running)
4404            .unwrap_or(false)
4405    }
4406
4407    fn settle_one_restored_message_failure(
4408        &mut self,
4409        event: Event,
4410        telemetry: bool,
4411        running: bool,
4412    ) {
4413        if running {
4414            self.emit_settlement(event, telemetry);
4415            return;
4416        }
4417        // Every other accumulation on the restore path has an explicit ceiling
4418        // and logs what it dropped; this one is retained until `start()`, which
4419        // may never come, so it gets the same treatment. Keeping the *oldest*
4420        // is deliberate: the settlements a restore produces first are the ones
4421        // for records it examined first, and dropping those in favour of later
4422        // ones would bias the survivors by backend listing order.
4423        if self.deferred_restore_settlements.len() >= MAX_DEFERRED_RESTORE_SETTLEMENTS {
4424            self.suppressed_restore_settlements += 1;
4425            return;
4426        }
4427        self.deferred_restore_settlements.push((event, telemetry));
4428    }
4429
4430    /// Returns an iterator over outbox messages (test-only).
4431    #[cfg(test)]
4432    pub(crate) fn outbox_messages(&self) -> impl Iterator<Item = &Message> {
4433        self.outbox.values().map(|e| &e.message)
4434    }
4435
4436    /// Returns the total number of entries across outbox and media_outbox (test-only).
4437    #[cfg(test)]
4438    pub(crate) fn outbox_entry_count(&self) -> usize {
4439        self.outbox.len() + self.media_outbox.len()
4440    }
4441
4442    /// Clears all outbox entries (test-only).
4443    #[cfg(test)]
4444    pub(crate) fn clear_outbox(&mut self) {
4445        self.outbox.clear();
4446    }
4447
4448    /// Returns `true` when `message_id` is anywhere in the reliability
4449    /// machinery — awaiting an ACK, held in the outbox, or queued for retry
4450    /// (test-only).
4451    ///
4452    /// Exists for the fire-and-forget frames (relay hints, delivery ACKs),
4453    /// whose correctness is defined by leaving *no* trace here: an un-ACKable
4454    /// frame that registers a pending ACK is retransmitted until the retry cap
4455    /// and then reported failed.
4456    #[cfg(test)]
4457    pub(crate) fn is_tracked_for_delivery(&self, message_id: &MessageId) -> bool {
4458        self.ack_manager.is_waiting_for_ack(message_id)
4459            || self.outbox.contains_key(message_id)
4460            || self.media_outbox.contains_key(message_id)
4461            || self.retry_queue.contains(&message_id.as_str())
4462    }
4463
4464    /// Returns a reference to the MLS manager Arc (test-only).
4465    #[cfg(test)]
4466    pub(crate) fn mls_manager_for_testing(&self) -> &Arc<RwLock<offline_protocol_mls::MlsManager>> {
4467        self.mls_manager
4468            .as_ref()
4469            .expect("MLS not initialized in test")
4470    }
4471
4472    /// Acquires a read guard on the MLS manager.
4473    ///
4474    /// Returns an error if MLS is not initialized or the lock is poisoned.
4475    pub(crate) fn read_mls_guard(
4476        &self,
4477    ) -> Result<std::sync::RwLockReadGuard<'_, offline_protocol_mls::MlsManager>> {
4478        let mls = self
4479            .mls_manager
4480            .as_ref()
4481            .ok_or_else(|| Error::Other("MLS not initialized".to_string()))?;
4482        mls.read()
4483            .map_err(|_| Error::Other("MLS lock poisoned".to_string()))
4484    }
4485}
4486
4487#[cfg(test)]
4488pub(crate) mod tests;