umsh_text/engine/
mod.rs

1//! Deterministic sans-I/O text-message engine.
2//!
3//! The engine is a reducer: platform code feeds it commands (compose,
4//! receive, transmit updates, archive results, ticks) and drains a bounded
5//! queue of effects and events. It never calls a database, sleeps, or
6//! transmits directly, so it can be tested without a radio, clock, executor,
7//! or platform runtime, and runs identically on mobile and pager targets.
8//!
9//! ## Output contract
10//!
11//! Outputs carry stable IDs and may be re-emitted; platform storage must
12//! apply message mutations idempotently (a mutation with a `revision` not
13//! newer than the last applied for its handle is a no-op). Rendered bodies
14//! live in an internal arena addressed by [`BodyRef`]; drain all outputs
15//! after each command — the arena resets once the queue is empty.
16
17pub mod fragment;
18pub mod repair;
19pub mod sequence;
20
21use heapless::{Deque, FnvIndexMap};
22use umsh_core::PublicKey;
23
24use crate::ParseError;
25use crate::codec;
26use crate::model::{
27    ConversationKey, FRAGMENT_BODY_MAX, FRAGMENT_COUNT_MAX, Fragment, MessageSequence, MessageType,
28    REASSEMBLED_BODY_MAX, Regarding, SenderScope, TextMessage,
29};
30use crate::validate::{self, Envelope, TextProfile, ValidateError, Validated};
31
32use fragment::{FragmentPlan, InsertOutcome, ReassemblyPool, RenderSentinels, empty_slot};
33use repair::{CoalesceRing, JitterSource, PendingLookup};
34use sequence::{
35    InboundStream, MessageHandle, OutboundStream, PendingRepair, SerialClass, StreamKey, classify,
36};
37
38/// Maximum encoded text payload the engine will hand to a transport.
39pub const MAX_FRAME: usize = 240;
40
41const ARENA_SIZE: usize = 4096;
42
43/// Tuning knobs. Defaults suit a LoRa mesh; a pager may shrink the windows.
44#[derive(Clone, Debug)]
45pub struct EngineConfig {
46    /// Grace period before acting on an inferred gap, absorbing reordering.
47    pub reorder_grace_ms: u64,
48    /// Minimum quiet time after the newest stored fragment before repair of
49    /// that reassembly may begin. Every fragment arrival defers repair by at
50    /// least this much (and by twice the observed inter-fragment gap when
51    /// that is larger), so an actively delivering message is never repaired
52    /// mid-flight. Platforms should set this to several frame airtimes.
53    pub fragment_grace_ms: u64,
54    /// Maximum extra randomized delay for channel-group repair requests.
55    pub group_jitter_ms: u64,
56    /// Largest forward gap repaired automatically (spec bound: 8).
57    pub max_auto_repair_gap: u8,
58    /// Minimum interval between resend requests on one stream.
59    pub min_request_interval_ms: u64,
60    /// Maximum resend requests transmitted per tick across all streams.
61    pub max_requests_per_tick: u8,
62    /// Maximum automatic request attempts per missing frame.
63    pub max_repair_attempts: u8,
64    /// Reassembly lifetime before a partial message is finalized.
65    pub reassembly_ttl_ms: u64,
66    /// Interval between repair attempts for the same frame.
67    pub request_retry_ms: u64,
68    /// Window in which duplicate resend requests are coalesced.
69    pub coalesce_window_ms: u64,
70    /// Latency after the first fragment of an incomplete message before the
71    /// host is notified anyway (the sooner of this deadline and completion).
72    pub fragment_notify_ms: u64,
73    pub sentinels: RenderSentinels,
74}
75
76impl Default for EngineConfig {
77    fn default() -> Self {
78        Self {
79            reorder_grace_ms: 2_000,
80            fragment_grace_ms: 8_000,
81            group_jitter_ms: 4_000,
82            max_auto_repair_gap: 8,
83            min_request_interval_ms: 2_000,
84            max_requests_per_tick: 4,
85            max_repair_attempts: 4,
86            reassembly_ttl_ms: 90_000,
87            request_retry_ms: 8_000,
88            coalesce_window_ms: 10_000,
89            fragment_notify_ms: 30_000,
90            sentinels: RenderSentinels::default(),
91        }
92    }
93}
94
95/// Where a transmit effect should be sent.
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub enum Destination {
98    /// Unicast to an authenticated peer.
99    Peer(PublicKey),
100    /// Multicast to a channel.
101    Channel(umsh_core::ChannelTag),
102    /// Blind-unicast to a peer over a channel key.
103    ChannelPeer {
104        channel: umsh_core::ChannelTag,
105        peer: PublicKey,
106    },
107}
108
109/// Archive key identifying resendable outbound material.
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
111pub struct ArchiveKey {
112    pub conversation: ConversationKey,
113    pub message_id: u8,
114    pub fragment: Option<u8>,
115}
116
117/// A frame for the platform to transmit.
118#[derive(Clone, Debug)]
119pub struct Transmission {
120    pub transmission_id: u32,
121    pub destination: Destination,
122    /// When present, the platform should archive this payload as resendable
123    /// material under this key (control frames carry `None`).
124    pub archive: Option<ArchiveKey>,
125    /// Encoded text-message payload (without the payload-type byte).
126    pub payload: heapless::Vec<u8, MAX_FRAME>,
127}
128
129/// Reference into the engine's render arena; resolve with [`Engine::body`].
130/// Valid until the output queue has been fully drained.
131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
132pub struct BodyRef {
133    offset: u16,
134    len: u16,
135}
136
137/// A protocol-level reference, resolved to a stable handle when unambiguous.
138#[derive(Clone, Copy, Debug, PartialEq, Eq)]
139pub enum ResolvedRef {
140    Handle(MessageHandle),
141    Unresolved(crate::model::WireRef),
142}
143
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub enum Direction {
146    Inbound,
147    Outbound,
148}
149
150/// Presence of a message's ordered transcript slot.
151///
152/// Orthogonal to [`CompletionStatus`]: a `Present` message may still be
153/// `Partial` (fragments outstanding), and a `GapPending` placeholder carries
154/// no body at all.
155#[derive(Clone, Copy, Debug, PartialEq, Eq)]
156pub enum Presence {
157    /// A real message occupies the slot.
158    Present,
159    /// A gap was detected; the slot is reserved and a repair is outstanding.
160    GapPending,
161    /// The gap could not be repaired (exhausted, expired, or disclaimed by
162    /// the sender); the slot is a permanent loss marker.
163    Unavailable,
164}
165
166/// Completeness of a message's body.
167#[derive(Clone, Copy, Debug, PartialEq, Eq)]
168pub enum CompletionStatus {
169    Complete,
170    Partial {
171        /// Bitmap of fragments present.
172        present: u16,
173        count: u8,
174        /// No further repair will occur; the render is final.
175        finalized: bool,
176    },
177}
178
179/// An idempotent transcript mutation.
180#[derive(Clone, Copy, Debug)]
181pub struct MessageMutation {
182    pub handle: MessageHandle,
183    /// Monotonic across all mutations; apply only if newer than the last
184    /// applied revision for this handle.
185    pub revision: u32,
186    pub kind: MutationKind,
187}
188
189#[derive(Clone, Copy, Debug)]
190pub enum MutationKind {
191    /// Create a transcript record.
192    Insert {
193        conversation: ConversationKey,
194        sender: SenderScope,
195        direction: Direction,
196        message_type: MessageType,
197        wire_id: Option<u8>,
198        epoch: u16,
199        /// Correlates an outbound record with the caller's compose call.
200        client_token: Option<u32>,
201        sender_handle: Option<BodyRef>,
202        regarding: Option<ResolvedRef>,
203        bg_color: Option<[u8; 3]>,
204        text_color: Option<[u8; 3]>,
205        body: BodyRef,
206        status: CompletionStatus,
207        /// Ordered-slot presence (real message, reserved gap, or lost gap).
208        presence: Presence,
209        /// This record fills a slot reserved earlier by a gap placeholder, so
210        /// it arrived out of order and should be flagged "received late".
211        late: bool,
212        /// The user should be notified of this record (engine-owned
213        /// eligibility: single-frame arrival, fragment completion, or the
214        /// fragment notify deadline). Never set for placeholders or control.
215        notify: bool,
216    },
217    /// Replace the rendered body (reassembly progress or finalization).
218    UpdateBody {
219        body: BodyRef,
220        status: CompletionStatus,
221        /// See [`MutationKind::Insert::late`].
222        late: bool,
223        /// See [`MutationKind::Insert::notify`].
224        notify: bool,
225    },
226    /// Apply an edit to the referenced original message.
227    Edit {
228        conversation: ConversationKey,
229        original: ResolvedRef,
230        body: BodyRef,
231    },
232    /// Mark the referenced original message deleted (empty edit).
233    Delete {
234        conversation: ConversationKey,
235        original: ResolvedRef,
236    },
237}
238
239#[derive(Clone, Copy, Debug, PartialEq, Eq)]
240pub enum DeliveryState {
241    Sent,
242    Acked,
243    Failed,
244}
245
246#[derive(Clone, Copy, Debug, PartialEq, Eq)]
247pub enum RepairOutcome {
248    Repaired,
249    Unavailable,
250    Exhausted,
251    Expired,
252    Unaddressable,
253}
254
255/// Application-visible events.
256#[derive(Clone, Copy, Debug, PartialEq, Eq)]
257pub enum Event {
258    DeliveryStateChanged {
259        handle: MessageHandle,
260        fragment: Option<u8>,
261        state: DeliveryState,
262    },
263    RepairStarted {
264        conversation: ConversationKey,
265        sender: SenderScope,
266        message_id: u8,
267        fragment: Option<u8>,
268    },
269    RepairFinished {
270        conversation: ConversationKey,
271        sender: SenderScope,
272        message_id: u8,
273        outcome: RepairOutcome,
274    },
275    /// The remote sender reported the named frame unavailable.
276    MessageUnavailable {
277        conversation: ConversationKey,
278        sender: SenderScope,
279        message_id: u8,
280        fragment: Option<u8>,
281    },
282}
283
284/// Non-fatal observations, surfaced for logging and counters.
285#[derive(Clone, Copy, Debug, PartialEq, Eq)]
286pub enum Diagnostic {
287    ParseFailed(ParseError),
288    ValidateFailed(ValidateError),
289    DuplicateMessage {
290        message_id: u8,
291    },
292    DuplicateFragment {
293        message_id: u8,
294        fragment: u8,
295    },
296    /// Conflicting bytes for an already-filled fragment slot were discarded.
297    FragmentConflict {
298        message_id: u8,
299        fragment: u8,
300    },
301    /// A fragment body above the wire maximum was dropped unstored.
302    OversizedFragment {
303        message_id: u8,
304        fragment: u8,
305    },
306    /// A fragment count above the wire maximum was dropped unassembled.
307    FragmentCountExceeded {
308        message_id: u8,
309        count: u8,
310    },
311    /// Fragments of one message disagreed about the fragment count.
312    FragmentCountMismatch {
313        message_id: u8,
314    },
315    /// Two known peer keys collide on one source hint; automatic repair is
316    /// suppressed for the merged stream.
317    HintCollision {
318        conversation: ConversationKey,
319    },
320    /// A reassembly was evicted under memory pressure before completion.
321    ReassemblyEvicted {
322        message_id: u8,
323    },
324    /// A stream table was full and its least-recently-active entry evicted.
325    StreamEvicted,
326    /// Presentation options were duplicated; first occurrences were kept.
327    RepeatedPresentationOptions {
328        mask: u16,
329    },
330    /// A continuation fragment carried ignored message-level metadata.
331    IgnoredContinuationMetadata {
332        message_id: u8,
333    },
334    /// The render arena or output queue overflowed; see `lost_outputs`.
335    OutputOverflow,
336    /// An archive result arrived for an unknown request.
337    UnknownLookup {
338        request_id: u32,
339    },
340    /// A completely reassembled body failed UTF-8 validation (the spec
341    /// validates only after every fragment is present); invalid sequences
342    /// were replaced with U+FFFD in the rendered text.
343    ReassembledInvalidUtf8 {
344        message_id: u8,
345    },
346    /// A resend request was ignored because an equivalent one was answered
347    /// within the coalescing window.
348    CoalescedResend {
349        message_id: u8,
350    },
351    /// A resend request arrived in a form the engine could not attribute.
352    UnattributableResend,
353}
354
355/// One drained output: an effect for the platform or an application event.
356#[derive(Clone, Debug)]
357pub enum Output {
358    Transmit(Transmission),
359    /// Persist the outbound stream checkpoint *before* transmitting the
360    /// frames queued after it.
361    ///
362    /// Failure contract: outputs are ordered, so the platform sees this
363    /// before the [`Output::Transmit`]s it covers. If the write fails, the
364    /// platform must drop (not send) those transmissions and resynchronize
365    /// via [`Engine::restore`]; sending them anyway risks reusing a wire ID
366    /// after the next power cycle without announcing a Sequence Reset.
367    StoreCheckpoint {
368        conversation: ConversationKey,
369        next_id: u8,
370        epoch: u16,
371    },
372    /// Look up resendable outbound material; answer with
373    /// [`Engine::archive_result`].
374    LookupOutbound {
375        request_id: u32,
376        conversation: ConversationKey,
377        sequence: MessageSequence,
378    },
379    /// Store resendable material without transmitting it. Emitted when an
380    /// edit re-issues the original message ID's archive with the edited
381    /// body: a later resend request for that ID must serve the edited
382    /// content, never the superseded original.
383    StoreArchive {
384        key: ArchiveKey,
385        payload: heapless::Vec<u8, MAX_FRAME>,
386    },
387    /// Delete every archived fragment stored under this message ID (the
388    /// unfragmented entry included). Emitted before an edit's replacement
389    /// [`Output::StoreArchive`]s — so stale fragments of a differently
390    /// fragmented original can never be served — and on delete, where the
391    /// retracted content must no longer be resendable at all.
392    DeleteArchive {
393        conversation: ConversationKey,
394        message_id: u8,
395    },
396    StoreMessage(MessageMutation),
397    Event(Event),
398    Diagnostic(Diagnostic),
399}
400
401/// Result of an outbound archive lookup.
402#[derive(Clone, Copy, Debug)]
403pub enum ArchiveResult<'a> {
404    /// The exact stored payload originally handed to `Transmit`.
405    Found {
406        payload: &'a [u8],
407    },
408    Deleted,
409    Evicted,
410    Unknown,
411}
412
413/// Persisted outbound stream checkpoint.
414#[derive(Clone, Copy, Debug, PartialEq, Eq)]
415pub struct StreamCheckpoint {
416    pub conversation: ConversationKey,
417    pub next_id: u8,
418    pub epoch: u16,
419}
420
421/// What the caller wants to send.
422#[derive(Clone, Copy, Debug)]
423pub enum ComposeIntent<'a> {
424    /// Plain text (`status` renders like IRC `/me`).
425    Text { body: &'a str, status: bool },
426    /// Reply to (`status: false`) or emote about (`status: true`) a message.
427    ///
428    /// An emote with an empty body withdraws the sender's earlier reaction.
429    Reply {
430        body: &'a str,
431        regarding: RegardingRef,
432        status: bool,
433    },
434    /// Replace a previously composed message's content.
435    Edit { original: ComposeRef, body: &'a str },
436    /// Delete a previously composed message (empty edit on the wire).
437    Delete { original: ComposeRef },
438}
439
440/// Reference to a previously composed original for edits and deletes.
441#[derive(Clone, Copy, Debug, PartialEq, Eq)]
442pub enum ComposeRef {
443    /// A message composed by this engine instance.
444    Handle(MessageHandle),
445    /// A persisted original from an earlier process: the wire ID and epoch
446    /// recorded when it was composed. Valid only while the outbound stream
447    /// is still in that epoch — a Sequence Reset invalidates older wire IDs
448    /// as reference targets — and while the ID is within the recently
449    /// allocated serial half-space.
450    Wire { message_id: u8, epoch: u16 },
451}
452
453/// What a reply or emote is about.
454///
455/// Unlike [`ComposeRef`], which only ever names the local stream, a regarding
456/// target is usually a message someone else sent, and usually one this
457/// process did not witness arrive: reacting to a message read yesterday is
458/// the ordinary case, not the exotic one. The wire form therefore carries the
459/// direction and — for channel groups, where the ID alone is meaningless —
460/// the target sender's hint.
461#[derive(Clone, Copy, Debug, PartialEq, Eq)]
462pub enum RegardingRef {
463    /// A message this engine instance inserted or composed.
464    Handle(MessageHandle),
465    /// A persisted message addressed by the coordinates recorded with it.
466    ///
467    /// `epoch` is checked against the stream when one is live, so a reference
468    /// that predates a Sequence Reset is refused rather than mis-targeted.
469    /// For an inbound target with no live stream the reference is sent
470    /// best-effort: retention is the receiver's business, and the receiving
471    /// platform matches unresolved references against its own stored rows.
472    Wire {
473        message_id: u8,
474        direction: Direction,
475        /// The target sender's hint. Required for inbound channel-group
476        /// targets, whose wire IDs are only meaningful per member.
477        sender_hint: Option<umsh_core::NodeHint>,
478        epoch: Option<u16>,
479    },
480}
481
482/// The stream a regarding target lives on, as far as the wire is concerned.
483fn regarding_sender(
484    conversation: ConversationKey,
485    direction: Direction,
486    sender_hint: Option<umsh_core::NodeHint>,
487) -> Option<SenderScope> {
488    match direction {
489        Direction::Outbound => Some(SenderScope::Local),
490        Direction::Inbound => match conversation {
491            ConversationKey::Direct { peer }
492            | ConversationKey::ChannelDirect { peer, .. }
493            | ConversationKey::Room { room: peer } => Some(SenderScope::Peer(peer)),
494            ConversationKey::ChannelGroup { .. } => sender_hint.map(SenderScope::ClaimedMember),
495        },
496    }
497}
498
499#[derive(Clone, Copy, Debug, PartialEq, Eq)]
500pub enum ComposeError {
501    /// The referenced message's wire ID is no longer in the outbound window.
502    UnknownOriginal,
503    /// The referenced message could not be resolved to a wire reference.
504    UnknownRegarding,
505    /// The body exceeds the wire maximum (10 fragments × 160 bytes).
506    TooLarge,
507    Encode(crate::EncodeError),
508}
509
510#[derive(Clone, Copy, Debug, PartialEq, Eq)]
511pub enum ReceiveError {
512    Parse(ParseError),
513    Validate(ValidateError),
514}
515
516/// The deterministic text engine.
517///
518/// `SLOTS`/`PAGES` size the shared reassembly pool (a page is 80 bytes; a
519/// full 160-byte fragment consumes two pages).
520pub struct Engine<P: TextProfile, const SLOTS: usize = 4, const PAGES: usize = 24> {
521    profile: P,
522    config: EngineConfig,
523    local_key: PublicKey,
524    jitter: JitterSource,
525    outbound: FnvIndexMap<ConversationKey, OutboundStream, 8>,
526    /// Sequence continuity for conversations outside the active `outbound`
527    /// map: restored checkpoints not yet composed to, and streams demoted by
528    /// eviction. Reactivation resumes from here instead of resetting.
529    cold_checkpoints: heapless::Vec<(ConversationKey, u8, u16), 24>,
530    inbound: FnvIndexMap<StreamKey, InboundStream, 16>,
531    pool: ReassemblyPool<SLOTS, PAGES>,
532    outputs: Deque<Output, 32>,
533    arena: [u8; ARENA_SIZE],
534    arena_used: usize,
535    next_handle: u32,
536    next_transmission: u32,
537    next_request: u32,
538    revision: u32,
539    /// A display name attached to our own group messages.
540    ///
541    /// A multicast reaches members who may hold no identity for us at all —
542    /// the wire carries only a 3-byte hint — so a group message says who sent
543    /// it or arrives anonymous. Unicast never needs it: the recipient
544    /// authenticated us by key.
545    local_handle: heapless::String<24>,
546    lookups: heapless::Vec<PendingLookup, 8>,
547    coalesce: CoalesceRing,
548    in_flight: heapless::Vec<InFlightFrame, 16>,
549    /// The platform has reported transport progress at least once, so
550    /// `in_flight` reflects real frame lifecycles rather than merely
551    /// emissions that were never confirmed either way.
552    saw_transmit_report: bool,
553    lost_outputs: u32,
554}
555
556/// A tracked outbound frame between `Transmit` emission and its terminal
557/// delivery report.
558#[derive(Clone, Copy, Debug)]
559struct InFlightFrame {
560    transmission_id: u32,
561    handle: MessageHandle,
562    fragment: Option<u8>,
563    /// Archive coordinates, used to coalesce resend requests that arrive
564    /// around this frame's own transmission.
565    archive: Option<ArchiveKey>,
566    /// Whether an acknowledgement will ever follow. Only a unicast to a
567    /// peer earns one; a multicast is answered by nobody, so `Sent` is the
568    /// last word its transport will ever say about it and the frame has to
569    /// retire there or it retires never.
570    expects_ack: bool,
571}
572
573impl<P: TextProfile, const SLOTS: usize, const PAGES: usize> Engine<P, SLOTS, PAGES> {
574    /// `jitter_seed` drives group-repair desynchronization only; it is
575    /// scheduling randomness, not security material.
576    pub fn new(profile: P, local_key: PublicKey, config: EngineConfig, jitter_seed: u64) -> Self {
577        Self {
578            profile,
579            config,
580            local_key,
581            jitter: JitterSource::new(jitter_seed),
582            outbound: FnvIndexMap::new(),
583            cold_checkpoints: heapless::Vec::new(),
584            inbound: FnvIndexMap::new(),
585            pool: ReassemblyPool::new(),
586            outputs: Deque::new(),
587            arena: [0; ARENA_SIZE],
588            arena_used: 0,
589            next_handle: 1,
590            next_transmission: 1,
591            next_request: 1,
592            revision: 1,
593            local_handle: heapless::String::new(),
594            lookups: heapless::Vec::new(),
595            coalesce: CoalesceRing::default(),
596            in_flight: heapless::Vec::new(),
597            saw_transmit_report: false,
598            lost_outputs: 0,
599        }
600    }
601
602    /// Drain the next output. The render arena resets when this returns
603    /// `None`, invalidating previously returned [`BodyRef`]s.
604    pub fn poll_output(&mut self) -> Option<Output> {
605        let output = self.outputs.pop_front();
606        if output.is_none() {
607            self.arena_used = 0;
608        }
609        output
610    }
611
612    /// Set the display name carried on our own group messages.
613    ///
614    /// Truncated to the wire limit on a character boundary. An empty name
615    /// clears it, and group messages then go out unnamed — recipients see
616    /// only the claimed hint.
617    pub fn set_local_handle(&mut self, handle: &str) {
618        self.local_handle.clear();
619        for character in handle.chars() {
620            if self.local_handle.push(character).is_err() {
621                break;
622            }
623        }
624    }
625
626    /// Resolve a [`BodyRef`] to its UTF-8 text.
627    pub fn body(&self, body: &BodyRef) -> &str {
628        let start = body.offset as usize;
629        let end = start + body.len as usize;
630        core::str::from_utf8(&self.arena[start..end]).unwrap_or("")
631    }
632
633    /// Outputs dropped because the queue overflowed (platform drained too
634    /// slowly). Nonzero values warrant a resync from persistent storage.
635    pub fn lost_outputs(&self) -> u32 {
636        self.lost_outputs
637    }
638
639    // ------------------------------------------------------------------
640    // Commands
641    // ------------------------------------------------------------------
642
643    /// Seed outbound stream continuity from persisted checkpoints.
644    /// Restored conversations resume their sequence at first use.
645    ///
646    /// Continuity is tracked in memory for 8 active plus 24 cold
647    /// conversations. Pass checkpoints **oldest-first**: when more are
648    /// supplied than the cold stash holds, the earliest entries are the
649    /// ones displaced. A conversation without retained continuity (never
650    /// restored, or displaced past the bound) starts a fresh epoch and
651    /// announces a lazy Sequence Reset — safe by design, at the cost of
652    /// receivers re-baselining that stream.
653    pub fn restore(&mut self, checkpoints: &[StreamCheckpoint], _now_ms: u64) {
654        for checkpoint in checkpoints {
655            self.stash_checkpoint(
656                checkpoint.conversation,
657                checkpoint.next_id,
658                checkpoint.epoch,
659            );
660        }
661    }
662
663    /// Compose and queue an outbound message. Returns the stable handle of
664    /// the affected transcript record.
665    pub fn compose(
666        &mut self,
667        conversation: ConversationKey,
668        client_token: u32,
669        intent: ComposeIntent<'_>,
670        now_ms: u64,
671    ) -> Result<MessageHandle, ComposeError> {
672        let (body, status_type, regarding_target, edit_original) = match intent {
673            ComposeIntent::Text { body, status } => (body, status, None, None),
674            ComposeIntent::Reply {
675                body,
676                status,
677                regarding,
678            } => (body, status, Some(regarding), None),
679            ComposeIntent::Edit { original, body } => (body, false, None, Some(original)),
680            ComposeIntent::Delete { original } => ("", false, None, Some(original)),
681        };
682        if body.len() > REASSEMBLED_BODY_MAX {
683            return Err(ComposeError::TooLarge);
684        }
685
686        // Wire references may target a stream restored from a checkpoint
687        // that has not composed yet in this process; materialize it first so
688        // its epoch is available for validation.
689        self.ensure_outbound(conversation, now_ms);
690        // Resolve references before allocating a wire ID.
691        let (regarding, regarding_ref) = match regarding_target {
692            None => (None, None),
693            Some(target) => {
694                let (wire, resolved) = self.resolve_regarding(conversation, target)?;
695                (Some(wire), Some(resolved))
696            }
697        };
698        let editing = match edit_original {
699            None => None,
700            Some(ComposeRef::Handle(handle)) => {
701                let stream = self.outbound.get(&conversation).expect("just ensured");
702                Some(
703                    stream
704                        .refs
705                        .lookup_handle(handle)
706                        .ok_or(ComposeError::UnknownOriginal)?,
707                )
708            }
709            Some(ComposeRef::Wire { message_id, epoch }) => {
710                let stream = self.outbound.get(&conversation).expect("just ensured");
711                if stream.epoch != epoch || stream.announce_reset {
712                    // The original predates a Sequence Reset (or the stream
713                    // lost continuity and will announce one); receivers have
714                    // discarded its wire ID as a reference target.
715                    return Err(ComposeError::UnknownOriginal);
716                }
717                // Best-effort recency guard: the ID must lie in the already
718                // allocated serial half-space. Receivers only retain refs
719                // for recent messages, so anything older dangles anyway.
720                let delta = stream.next_id.wrapping_sub(message_id);
721                if delta == 0 || delta > 128 {
722                    return Err(ComposeError::UnknownOriginal);
723                }
724                Some(message_id)
725            }
726        };
727
728        let stream = self.outbound.get_mut(&conversation).expect("just ensured");
729        stream.last_active_ms = now_ms;
730        let message_id = stream.allocate();
731        let announce_reset = core::mem::take(&mut stream.announce_reset);
732        let next_id = stream.next_id;
733        let epoch = stream.epoch;
734
735        // Commit the checkpoint advancement before any frame is released.
736        self.push_output(Output::StoreCheckpoint {
737            conversation,
738            next_id,
739            epoch,
740        });
741
742        let message_type = if status_type {
743            MessageType::Status
744        } else {
745            MessageType::Basic
746        };
747        // Named only where the recipient may not know us. A unicast peer
748        // authenticated us by key, so spending airtime on a name there would
749        // repeat what the frame already proves.
750        //
751        // Copied out rather than borrowed: encoding needs `&mut self`.
752        let handle_bytes: heapless::String<24> = match conversation {
753            ConversationKey::ChannelGroup { .. } => self.local_handle.clone(),
754            _ => heapless::String::new(),
755        };
756        let sender_handle = (!handle_bytes.is_empty()).then(|| handle_bytes.as_str());
757        let template = TextMessage {
758            message_type,
759            sender_handle,
760            sequence: Some(MessageSequence::unfragmented(message_id)),
761            sequence_reset: announce_reset,
762            regarding,
763            editing,
764            bg_color: None,
765            text_color: None,
766            channel_group_resend: false,
767            extensions: Default::default(),
768            body: body.as_bytes(),
769        };
770
771        let handle = self.alloc_handle();
772        self.encode_and_queue(conversation, handle, &template, body.as_bytes(), message_id)?;
773
774        // Record the mapping only for original messages; edit IDs must not
775        // become reference targets. They do get remembered as aliases, so a
776        // peer that references the edit still reaches the original.
777        let stream = self.outbound.get_mut(&conversation).expect("present");
778        match editing {
779            None => stream.refs.record(message_id, handle),
780            Some(original_id) => stream.edit_refs.record(message_id, original_id),
781        }
782
783        // Superseded content must never re-air. Retire the original ID's
784        // archived material; for an edit, re-issue it under the same ID with
785        // the edited body so a resend request serves current content (a
786        // delete leaves it empty, answered as Message Unavailable). The
787        // delete goes first so stale fragments of a differently fragmented
788        // original can never be served alongside the replacement.
789        if let Some(original_id) = editing {
790            self.push_output(Output::DeleteArchive {
791                conversation,
792                message_id: original_id,
793            });
794            if !body.is_empty() {
795                self.archive_replacement(conversation, original_id, body.as_bytes());
796            }
797        }
798
799        // Emit the transcript mutation.
800        let body_ref = self
801            .arena_store(body)
802            .unwrap_or(BodyRef { offset: 0, len: 0 });
803        // A wire-referenced original has no live handle in this process; the
804        // platform resolves the exported reference against its own persisted
805        // rows for the local sender's stream.
806        let original_ref = edit_original.map(|original| match original {
807            ComposeRef::Handle(handle) => ResolvedRef::Handle(handle),
808            ComposeRef::Wire { message_id, .. } => {
809                ResolvedRef::Unresolved(crate::model::WireRef::SenderScoped {
810                    sender: SenderScope::Local,
811                    message_id,
812                })
813            }
814        });
815        let kind = match (original_ref, body.is_empty()) {
816            (Some(original), true) => MutationKind::Delete {
817                conversation,
818                original,
819            },
820            (Some(original), false) => MutationKind::Edit {
821                conversation,
822                original,
823                body: body_ref,
824            },
825            (None, _) => MutationKind::Insert {
826                conversation,
827                sender: SenderScope::Local,
828                direction: Direction::Outbound,
829                message_type,
830                wire_id: Some(message_id),
831                epoch,
832                client_token: Some(client_token),
833                sender_handle: None,
834                regarding: regarding_ref,
835                bg_color: None,
836                text_color: None,
837                body: body_ref,
838                status: CompletionStatus::Complete,
839                presence: Presence::Present,
840                late: false,
841                notify: false,
842            },
843        };
844        self.emit_mutation(handle, kind);
845        Ok(handle)
846    }
847
848    /// Feed one MAC-validated received text payload (without the
849    /// payload-type byte) through the engine.
850    ///
851    /// `sender_full_key` resolves a claimed multicast member to a full public
852    /// key when the platform knows it; it is required for that sender's
853    /// streams to be repairable.
854    pub fn receive(
855        &mut self,
856        envelope: &Envelope,
857        sender_full_key: Option<PublicKey>,
858        payload: &[u8],
859        now_ms: u64,
860    ) -> Result<(), ReceiveError> {
861        let (message, info) = codec::parse_with_info(payload).map_err(|error| {
862            self.push_output(Output::Diagnostic(Diagnostic::ParseFailed(error)));
863            ReceiveError::Parse(error)
864        })?;
865        let (validated, notes) = validate::validate(&self.profile, envelope, &message, &info)
866            .map_err(|error| {
867                self.push_output(Output::Diagnostic(Diagnostic::ValidateFailed(error)));
868                ReceiveError::Validate(error)
869            })?;
870        if notes.repeated_presentation_mask != 0 {
871            self.push_output(Output::Diagnostic(
872                Diagnostic::RepeatedPresentationOptions {
873                    mask: notes.repeated_presentation_mask,
874                },
875            ));
876        }
877
878        match validated {
879            Validated::Content(content) => {
880                if notes.ignored_continuation_metadata
881                    && let Some(sequence) = content.sequence
882                {
883                    self.push_output(Output::Diagnostic(
884                        Diagnostic::IgnoredContinuationMetadata {
885                            message_id: sequence.message_id,
886                        },
887                    ));
888                }
889                self.receive_content(envelope, sender_full_key, &content, now_ms);
890            }
891            Validated::ResendRequest {
892                sequence,
893                channel_group,
894            } => self.receive_resend_request(envelope, sequence, channel_group, now_ms),
895            Validated::Unavailable { sequence } => {
896                self.receive_unavailable(envelope, sequence, now_ms)
897            }
898        }
899        Ok(())
900    }
901
902    /// Report transport progress for a previously emitted transmission.
903    pub fn transmit_update(&mut self, transmission_id: u32, state: DeliveryState, now_ms: u64) {
904        self.saw_transmit_report = true;
905        let Some(position) = self
906            .in_flight
907            .iter()
908            .position(|frame| frame.transmission_id == transmission_id)
909        else {
910            return;
911        };
912        let frame = self.in_flight[position];
913        self.push_output(Output::Event(Event::DeliveryStateChanged {
914            handle: frame.handle,
915            fragment: frame.fragment,
916            state,
917        }));
918        // `Sent` is not terminal for a frame that is still waiting to be
919        // acknowledged, but it is the whole story for one that never will
920        // be. Holding a multicast here forever would make this node refuse
921        // every resend request for it — the one thing a group member has no
922        // other way to recover from.
923        let terminal = match state {
924            DeliveryState::Acked | DeliveryState::Failed => true,
925            DeliveryState::Sent => !frame.expects_ack,
926        };
927        if terminal {
928            self.in_flight.remove(position);
929        }
930        let _ = now_ms;
931    }
932
933    /// Answer a previously emitted `LookupOutbound` effect.
934    pub fn archive_result(&mut self, request_id: u32, result: ArchiveResult<'_>, now_ms: u64) {
935        let Some(position) = self
936            .lookups
937            .iter()
938            .position(|lookup| lookup.request_id == request_id)
939        else {
940            self.push_output(Output::Diagnostic(Diagnostic::UnknownLookup { request_id }));
941            return;
942        };
943        let lookup = self.lookups.remove(position);
944        let destination = destination_for(&lookup.conversation);
945
946        match result {
947            ArchiveResult::Found { payload } if payload.len() <= MAX_FRAME => {
948                let mut frame = heapless::Vec::new();
949                let _ = frame.extend_from_slice(payload);
950                // Track the re-transmission against the *original* outbound
951                // message's handle so its ack flips the original row from
952                // "Not Delivered" back to "Delivered", rather than dangling a
953                // throwaway handle. Falls back to untracked when the original
954                // ref has aged out of the outbound window.
955                let track = self
956                    .outbound
957                    .get(&lookup.conversation)
958                    .and_then(|stream| stream.refs.lookup(lookup.sequence.message_id))
959                    .map(|handle| {
960                        (
961                            handle,
962                            lookup.sequence.fragment.map(|fragment| fragment.index),
963                        )
964                    });
965                self.queue_transmit(destination, None, frame, track);
966            }
967            _ => {
968                // Deleted, evicted, unknown, or oversized stored material:
969                // answer Message Unavailable for the requested frame.
970                let response = TextMessage {
971                    message_type: MessageType::MessageUnavailable,
972                    sequence: Some(lookup.sequence),
973                    ..TextMessage::basic("")
974                };
975                let mut buffer = [0u8; MAX_FRAME];
976                if let Ok(len) = codec::encode(&response, &mut buffer) {
977                    let mut frame = heapless::Vec::new();
978                    let _ = frame.extend_from_slice(&buffer[..len]);
979                    self.queue_transmit(destination, None, frame, None);
980                }
981            }
982        }
983        self.coalesce
984            .record(lookup.conversation, &lookup.sequence, now_ms);
985    }
986
987    /// Advance timers: reassembly expiry, notify deadlines, repair scheduling.
988    pub fn tick(&mut self, now_ms: u64) {
989        self.expire_slots(now_ms);
990        self.notify_deadlines(now_ms);
991        self.schedule_fragment_repairs(now_ms);
992        self.transmit_due_repairs(now_ms);
993    }
994
995    /// Notify the host about a fragmented message that has stalled incomplete
996    /// past the notify deadline (`first_fragment_ms + fragment_notify_ms`),
997    /// emitting one partial-body update flagged for notification. Completion
998    /// notifies earlier via `publish_slot`; the `notified` bit fires once.
999    fn notify_deadlines(&mut self, now_ms: u64) {
1000        for index in 0..SLOTS {
1001            let due = self.pool.slots[index].as_ref().is_some_and(|slot| {
1002                slot.announced
1003                    && !slot.notified
1004                    && !slot.is_complete()
1005                    && now_ms >= slot.created_ms + self.config.fragment_notify_ms
1006            });
1007            if !due {
1008                continue;
1009            }
1010            let body = self.render_to_arena(index, false);
1011            let slot = self.pool.slots[index].as_mut().expect("occupied");
1012            slot.notified = true;
1013            let status = CompletionStatus::Partial {
1014                present: slot.present,
1015                count: slot.count,
1016                finalized: false,
1017            };
1018            let handle = slot.handle;
1019            self.emit_mutation(
1020                handle,
1021                MutationKind::UpdateBody {
1022                    body,
1023                    status,
1024                    late: false,
1025                    notify: true,
1026                },
1027            );
1028        }
1029    }
1030
1031    // ------------------------------------------------------------------
1032    // Content path
1033    // ------------------------------------------------------------------
1034
1035    fn receive_content(
1036        &mut self,
1037        envelope: &Envelope,
1038        sender_full_key: Option<PublicKey>,
1039        content: &validate::ContentMessage<'_>,
1040        now_ms: u64,
1041    ) {
1042        let key = StreamKey {
1043            conversation: envelope.conversation,
1044            sender: envelope.sender,
1045        };
1046        self.ensure_inbound(key, now_ms);
1047        let stream = self.inbound.get_mut(&key).expect("just ensured");
1048        stream.last_active_ms = now_ms;
1049
1050        // Hint-collision containment.
1051        if let Some(full_key) = sender_full_key {
1052            match stream.sender_key {
1053                None => stream.sender_key = Some(full_key),
1054                Some(existing) if existing != full_key => {
1055                    if !stream.collided {
1056                        stream.collided = true;
1057                        let conversation = envelope.conversation;
1058                        self.push_output(Output::Diagnostic(Diagnostic::HintCollision {
1059                            conversation,
1060                        }));
1061                    }
1062                }
1063                Some(_) => {}
1064            }
1065        }
1066
1067        let sequence = content.sequence;
1068        if content.sequence_reset {
1069            let repeated_id = sequence.is_some_and(|sequence| {
1070                self.inbound
1071                    .get(&key)
1072                    .is_some_and(|stream| stream.seen.contains(sequence.message_id))
1073            });
1074            if !repeated_id {
1075                let stream = self.inbound.get_mut(&key).expect("present");
1076                // The arriving message itself establishes the new baseline
1077                // below. A retransmitted reset-bearing ID is still a
1078                // duplicate; applying its reset again would erase the very
1079                // state needed to suppress it.
1080                let orphans: heapless::Vec<(u8, MessageHandle), 8> = stream
1081                    .pending
1082                    .iter()
1083                    .filter_map(|pending| pending.handle.map(|h| (pending.message_id, h)))
1084                    .collect();
1085                stream.reset_epoch(None);
1086                self.pool.drop_stream(&key);
1087                for (missing, handle) in orphans {
1088                    self.flip_placeholder_unavailable(key, missing, handle);
1089                }
1090            }
1091        }
1092
1093        let Some(sequence) = sequence else {
1094            // Unsequenced: display-only, unreferencable, no dedup possible.
1095            self.insert_content(
1096                envelope,
1097                content,
1098                None,
1099                CompletionStatus::Complete,
1100                false,
1101                true,
1102                None,
1103                now_ms,
1104            );
1105            return;
1106        };
1107        let id = sequence.message_id;
1108
1109        // Sequence-window accounting (shared by fragments and whole
1110        // messages; fragment dedup happens against the slot bitmap).
1111        let stream = self.inbound.get_mut(&key).expect("present");
1112        let mut first_sighting = true;
1113        match stream.baseline {
1114            None => {
1115                stream.baseline = Some(id);
1116                stream.seen.insert(id);
1117            }
1118            Some(baseline) => match classify(baseline, id) {
1119                SerialClass::Baseline => first_sighting = false,
1120                SerialClass::Older(_) => {
1121                    if stream.seen.contains(id) {
1122                        first_sighting = false;
1123                    } else {
1124                        stream.seen.insert(id);
1125                    }
1126                }
1127                SerialClass::Newer(delta) => {
1128                    let gap = delta - 1;
1129                    let collided = stream.collided;
1130                    let epoch = stream.epoch;
1131                    let can_repair = gap > 0
1132                        && gap <= self.config.max_auto_repair_gap
1133                        && !collided
1134                        && content.sequence.is_some();
1135                    if can_repair {
1136                        let group =
1137                            matches!(envelope.conversation, ConversationKey::ChannelGroup { .. });
1138                        let base_deadline = now_ms + self.config.reorder_grace_ms;
1139                        for step in 1..=gap {
1140                            let missing = baseline.wrapping_add(step);
1141                            let jitter = if group {
1142                                self.jitter.jitter_ms(self.config.group_jitter_ms)
1143                            } else {
1144                                0
1145                            };
1146                            // Reserve the ordered slot now, at the live edge:
1147                            // the backfilled frame fills this same handle in
1148                            // place instead of landing at the transcript
1149                            // bottom. Emitted *before* the triggering
1150                            // message's Insert so its rowid sorts above.
1151                            let placeholder = self.emit_gap_placeholder(key, missing, epoch);
1152                            let stream = self.inbound.get_mut(&key).expect("present");
1153                            let _ = stream.pending.push(PendingRepair {
1154                                message_id: missing,
1155                                fragment: None,
1156                                deadline_ms: base_deadline + jitter,
1157                                attempts: 0,
1158                                handle: Some(placeholder),
1159                            });
1160                        }
1161                    }
1162                    let stream = self.inbound.get_mut(&key).expect("present");
1163                    stream.seen.advance(baseline, delta);
1164                    stream.baseline = Some(id);
1165                    stream.seen.insert(id);
1166                }
1167                SerialClass::Ambiguous => {
1168                    // Re-baseline without backfill or epoch change; any
1169                    // outstanding gap placeholders can no longer be repaired.
1170                    let orphans: heapless::Vec<(u8, MessageHandle), 8> = stream
1171                        .pending
1172                        .iter()
1173                        .filter_map(|pending| pending.handle.map(|h| (pending.message_id, h)))
1174                        .collect();
1175                    stream.seen.clear();
1176                    stream.pending.clear();
1177                    stream.baseline = Some(id);
1178                    stream.seen.insert(id);
1179                    for (missing, handle) in orphans {
1180                        self.flip_placeholder_unavailable(key, missing, handle);
1181                    }
1182                }
1183            },
1184        }
1185
1186        // A fragmented ID may legitimately appear many times while its slot
1187        // is open, once per distinct fragment and again for repairs. After
1188        // completion, expiry, or eviction closes that slot, however, any
1189        // already-seen fragment is late duplicate traffic. Never let it open
1190        // a second slot and render the same logical message again.
1191        if let Some(fragment) = sequence.fragment {
1192            let epoch = self.inbound.get(&key).expect("present").epoch;
1193            if !first_sighting && self.pool.find_slot(&key, epoch, id).is_none() {
1194                self.push_output(Output::Diagnostic(Diagnostic::DuplicateFragment {
1195                    message_id: id,
1196                    fragment: fragment.index,
1197                }));
1198                return;
1199            }
1200        }
1201
1202        // An arrival satisfies pending repair for its frame.
1203        let stream = self.inbound.get_mut(&key).expect("present");
1204        let fragment_index = sequence.fragment.map(|fragment| fragment.index);
1205        let was_repairing = stream
1206            .pending
1207            .iter()
1208            .any(|pending| pending.message_id == id && pending.attempts > 0);
1209        stream.cancel_pending(id, fragment_index);
1210        if was_repairing && sequence.fragment.is_none() {
1211            self.push_output(Output::Event(Event::RepairFinished {
1212                conversation: key.conversation,
1213                sender: key.sender,
1214                message_id: id,
1215                outcome: RepairOutcome::Repaired,
1216            }));
1217        }
1218
1219        match sequence.fragment {
1220            Some(fragment) => {
1221                self.receive_fragment(envelope, content, key, id, fragment, now_ms);
1222            }
1223            None => {
1224                if !first_sighting {
1225                    self.push_output(Output::Diagnostic(Diagnostic::DuplicateMessage {
1226                        message_id: id,
1227                    }));
1228                    return;
1229                }
1230                self.receive_single_frame(envelope, content, key, id, now_ms);
1231            }
1232        }
1233    }
1234
1235    fn receive_single_frame(
1236        &mut self,
1237        envelope: &Envelope,
1238        content: &validate::ContentMessage<'_>,
1239        key: StreamKey,
1240        id: u8,
1241        now_ms: u64,
1242    ) {
1243        // A handle already registered for this wire ID is a gap placeholder
1244        // reserved earlier at the live edge — this frame fills it in place.
1245        let placeholder = self
1246            .inbound
1247            .get(&key)
1248            .and_then(|stream| stream.refs.lookup(id));
1249
1250        if let Some(mut original_id) = content.editing {
1251            if let Some(stream) = self.inbound.get_mut(&key) {
1252                // An edit may itself name an earlier edit; collapse the chain
1253                // so every later reference lands on the slot the transcript
1254                // actually holds, and remember this edit as an alias for it.
1255                original_id = stream.edit_refs.resolve(original_id);
1256                stream.edit_refs.record(id, original_id);
1257            }
1258            // The backfilled frame turned out to be an edit, not a standalone
1259            // bubble: retire the spinner and apply the edit to its target.
1260            if let Some(handle) = placeholder {
1261                self.emit_mutation(
1262                    handle,
1263                    MutationKind::Delete {
1264                        conversation: key.conversation,
1265                        original: ResolvedRef::Handle(handle),
1266                    },
1267                );
1268                if let Some(stream) = self.inbound.get_mut(&key) {
1269                    stream.refs.retire(id);
1270                }
1271            }
1272            // An edit whose *target* is a still-missing gap: the edit already
1273            // carries that slot's current content, so fill (or, for a delete,
1274            // remove) the placeholder instead of spinning until the repair of
1275            // superseded content exhausts.
1276            if self.fill_gap_with_edit(envelope, key, original_id, content.body) {
1277                return;
1278            }
1279            // Edits and deletes target the sender's own stream.
1280            let original = self
1281                .inbound
1282                .get(&key)
1283                .and_then(|stream| stream.refs.lookup(original_id))
1284                .map(ResolvedRef::Handle)
1285                .unwrap_or(ResolvedRef::Unresolved(
1286                    crate::model::WireRef::SenderScoped {
1287                        sender: envelope.sender,
1288                        message_id: original_id,
1289                    },
1290                ));
1291            let handle = self.alloc_handle();
1292            let kind = if content.body.is_empty() {
1293                MutationKind::Delete {
1294                    conversation: key.conversation,
1295                    original,
1296                }
1297            } else {
1298                let body = core::str::from_utf8(content.body).unwrap_or("");
1299                let body_ref = self
1300                    .arena_store(body)
1301                    .unwrap_or(BodyRef { offset: 0, len: 0 });
1302                MutationKind::Edit {
1303                    conversation: key.conversation,
1304                    original,
1305                    body: body_ref,
1306                }
1307            };
1308            self.emit_mutation(handle, kind);
1309            return;
1310        }
1311
1312        let late = placeholder.is_some();
1313        let handle = self.insert_content(
1314            envelope,
1315            content,
1316            Some(id),
1317            CompletionStatus::Complete,
1318            late,
1319            true,
1320            placeholder,
1321            now_ms,
1322        );
1323        if let Some(stream) = self.inbound.get_mut(&key) {
1324            stream.refs.record(id, handle);
1325        }
1326    }
1327
1328    fn receive_fragment(
1329        &mut self,
1330        envelope: &Envelope,
1331        content: &validate::ContentMessage<'_>,
1332        key: StreamKey,
1333        id: u8,
1334        fragment: Fragment,
1335        now_ms: u64,
1336    ) {
1337        if fragment.count > FRAGMENT_COUNT_MAX {
1338            // Above the wire maximum: account for the ID, drop the assembly.
1339            self.push_output(Output::Diagnostic(Diagnostic::FragmentCountExceeded {
1340                message_id: id,
1341                count: fragment.count,
1342            }));
1343            return;
1344        }
1345        let epoch = self
1346            .inbound
1347            .get(&key)
1348            .map(|stream| stream.epoch)
1349            .unwrap_or(0);
1350
1351        let slot_index = match self.pool.find_slot(&key, epoch, id) {
1352            Some(index) => {
1353                let slot = self.pool.slots[index].as_ref().expect("occupied");
1354                if slot.count != fragment.count {
1355                    self.push_output(Output::Diagnostic(Diagnostic::FragmentCountMismatch {
1356                        message_id: id,
1357                    }));
1358                    return;
1359                }
1360                index
1361            }
1362            None => {
1363                // A handle already registered for this wire ID is a gap
1364                // placeholder reserved earlier; reuse it so the reassembly
1365                // fills that ordered slot in place.
1366                let placeholder = self
1367                    .inbound
1368                    .get(&key)
1369                    .and_then(|stream| stream.refs.lookup(id));
1370                let handle = placeholder.unwrap_or_else(|| self.alloc_handle());
1371                let mut slot = empty_slot(key, epoch, id, fragment.count, handle, now_ms);
1372                slot.late = placeholder.is_some();
1373                slot.deadline_ms = now_ms + self.config.reassembly_ttl_ms;
1374                let group = matches!(key.conversation, ConversationKey::ChannelGroup { .. });
1375                let jitter = if group {
1376                    self.jitter.jitter_ms(self.config.group_jitter_ms)
1377                } else {
1378                    0
1379                };
1380                slot.repair_at_ms = now_ms + self.config.fragment_grace_ms + jitter;
1381                match self.pool.open_slot(slot.clone()) {
1382                    Some(index) => index,
1383                    None => {
1384                        // Evict the oldest assembly to make room.
1385                        if let Some(oldest) = self.pool.oldest_slot() {
1386                            self.finalize_slot(oldest, now_ms, RepairOutcome::Expired, true);
1387                        }
1388                        match self.pool.open_slot(slot) {
1389                            Some(index) => index,
1390                            None => {
1391                                self.push_output(Output::Diagnostic(
1392                                    Diagnostic::ReassemblyEvicted { message_id: id },
1393                                ));
1394                                return;
1395                            }
1396                        }
1397                    }
1398                }
1399            }
1400        };
1401
1402        // Fragment zero carries the message-level metadata, which applies to
1403        // the entire reassembled message. Captured first-arrival-wins and
1404        // before storage, so even an oversized fragment zero contributes its
1405        // valid, authenticated options.
1406        if fragment.index == 0 {
1407            let slot = self.pool.slots[slot_index].as_mut().expect("occupied");
1408            if !slot.have_meta {
1409                slot.meta = fragment::FirstMeta {
1410                    message_type_byte: content.message_type.to_byte(),
1411                    regarding: content.regarding,
1412                    editing: content.editing,
1413                };
1414                slot.have_meta = true;
1415            }
1416        }
1417
1418        if content.body.len() > FRAGMENT_BODY_MAX {
1419            // Syntactically valid but beyond this receiver's storage (the
1420            // sender violated the wire maximum). Salvage the rest of the
1421            // message: mark just this fragment unavailable — a resend would
1422            // return the same oversized bytes — and let the assembly proceed
1423            // for every fragment we can hold.
1424            self.push_output(Output::Diagnostic(Diagnostic::OversizedFragment {
1425                message_id: id,
1426                fragment: fragment.index,
1427            }));
1428            let slot = self.pool.slots[slot_index].as_mut().expect("occupied");
1429            let bit = 1u16 << fragment.index;
1430            if slot.present & bit == 0 {
1431                slot.unavailable |= bit;
1432            }
1433            self.publish_slot(envelope, content, slot_index, now_ms);
1434            if self.pool.slots[slot_index]
1435                .as_ref()
1436                .is_some_and(|slot| slot.is_settled() && !slot.is_complete())
1437            {
1438                self.finalize_slot(slot_index, now_ms, RepairOutcome::Unavailable, false);
1439            }
1440            return;
1441        }
1442
1443        // Store the fragment bytes.
1444        let mut outcome = self
1445            .pool
1446            .insert_fragment(slot_index, fragment.index, content.body);
1447        if outcome == InsertOutcome::NoSpace {
1448            // Free pages by evicting the oldest *other* slot, then retry.
1449            let oldest = self.pool.oldest_slot().filter(|index| *index != slot_index);
1450            if let Some(oldest) = oldest {
1451                self.finalize_slot(oldest, now_ms, RepairOutcome::Expired, true);
1452                outcome = self
1453                    .pool
1454                    .insert_fragment(slot_index, fragment.index, content.body);
1455            }
1456        }
1457        match outcome {
1458            InsertOutcome::Stored => {}
1459            InsertOutcome::Duplicate => {
1460                self.push_output(Output::Diagnostic(Diagnostic::DuplicateFragment {
1461                    message_id: id,
1462                    fragment: fragment.index,
1463                }));
1464                return;
1465            }
1466            InsertOutcome::Conflict => {
1467                self.push_output(Output::Diagnostic(Diagnostic::FragmentConflict {
1468                    message_id: id,
1469                    fragment: fragment.index,
1470                }));
1471                return;
1472            }
1473            InsertOutcome::NoSpace => {
1474                self.push_output(Output::Diagnostic(Diagnostic::ReassemblyEvicted {
1475                    message_id: id,
1476                }));
1477                return;
1478            }
1479            InsertOutcome::TooLarge => {
1480                // Validation already rejects oversized bodies; this arm keeps
1481                // the pool guard observable if that ever regresses.
1482                self.push_output(Output::Diagnostic(Diagnostic::OversizedFragment {
1483                    message_id: id,
1484                    fragment: fragment.index,
1485                }));
1486                return;
1487            }
1488        }
1489
1490        // A stored fragment is proof the sender is still delivering: defer
1491        // repair by at least the configured grace, and by twice the observed
1492        // inter-fragment gap when the link is slower than that. Requesting a
1493        // resend of a frame the sender has merely not reached yet duplicates
1494        // it on air and delays the frames behind it — the repair timer must
1495        // only fire once arrivals actually stall.
1496        {
1497            let group = matches!(key.conversation, ConversationKey::ChannelGroup { .. });
1498            let jitter = if group {
1499                self.jitter.jitter_ms(self.config.group_jitter_ms)
1500            } else {
1501                0
1502            };
1503            let grace = self.config.fragment_grace_ms;
1504            let slot = self.pool.slots[slot_index].as_mut().expect("occupied");
1505            let gap = now_ms.saturating_sub(slot.last_fragment_ms);
1506            slot.last_fragment_ms = now_ms;
1507            let holdoff = grace.max(gap.saturating_mul(2));
1508            slot.repair_at_ms = slot.repair_at_ms.max(now_ms + holdoff + jitter);
1509        }
1510
1511        self.publish_slot(envelope, content, slot_index, now_ms);
1512        // A stored fragment can settle a slot that carries unavailable
1513        // marks; nothing further can improve it, so finalize now rather
1514        // than waiting for the reassembly TTL.
1515        if self.pool.slots[slot_index]
1516            .as_ref()
1517            .is_some_and(|slot| slot.is_settled() && !slot.is_complete())
1518        {
1519            self.finalize_slot(slot_index, now_ms, RepairOutcome::Unavailable, false);
1520        }
1521    }
1522
1523    /// Emit the appropriate mutation for a slot's current state, completing
1524    /// it if every fragment is present.
1525    ///
1526    /// `content` is the fragment that triggered this call. The announcing
1527    /// Insert always runs during the call that delivered fragment zero
1528    /// (`have_meta` is set in that same call), so presentation metadata —
1529    /// sender handle and colors — is borrowed from `content` at full
1530    /// fidelity instead of being retained in the slot.
1531    fn publish_slot(
1532        &mut self,
1533        envelope: &Envelope,
1534        content: &validate::ContentMessage<'_>,
1535        slot_index: usize,
1536        now_ms: u64,
1537    ) {
1538        let slot = self.pool.slots[slot_index].as_ref().expect("occupied");
1539        let key = slot.stream;
1540        let handle = slot.handle;
1541        let complete = slot.is_complete();
1542        let have_meta = slot.have_meta;
1543        let is_edit = slot.meta.editing.is_some();
1544        let announced = slot.announced;
1545        let late = slot.late;
1546        let notified = slot.notified;
1547        let id = slot.message_id;
1548        let present = slot.present;
1549        let count = slot.count;
1550
1551        if !have_meta {
1552            // Until fragment zero arrives we cannot know how to present the
1553            // message (it may be an edit); keep accumulating silently.
1554            return;
1555        }
1556
1557        if complete && is_edit {
1558            let mut original_id = self.pool.slots[slot_index]
1559                .as_ref()
1560                .expect("occupied")
1561                .meta
1562                .editing
1563                .expect("checked");
1564            if let Some(stream) = self.inbound.get_mut(&key) {
1565                original_id = stream.edit_refs.resolve(original_id);
1566                stream.edit_refs.record(id, original_id);
1567            }
1568            let original = self
1569                .inbound
1570                .get(&key)
1571                .and_then(|stream| stream.refs.lookup(original_id))
1572                .map(ResolvedRef::Handle)
1573                .unwrap_or(ResolvedRef::Unresolved(
1574                    crate::model::WireRef::SenderScoped {
1575                        sender: key.sender,
1576                        message_id: original_id,
1577                    },
1578                ));
1579            let body_ref = self.render_to_arena(slot_index, true);
1580            // A reassembly that reused a gap placeholder turned out to be an
1581            // edit, not a standalone bubble: retire the spinner row and apply
1582            // the edit under a fresh handle.
1583            if late {
1584                self.emit_mutation(
1585                    handle,
1586                    MutationKind::Delete {
1587                        conversation: key.conversation,
1588                        original: ResolvedRef::Handle(handle),
1589                    },
1590                );
1591                if let Some(stream) = self.inbound.get_mut(&key) {
1592                    stream.refs.retire(id);
1593                }
1594            }
1595            // An edit whose *target* is a still-missing gap fills that slot
1596            // with the edited content instead of dangling until the repair of
1597            // superseded content exhausts.
1598            {
1599                let mut scratch = [0u8; REASSEMBLED_BODY_MAX + 64];
1600                let len = (body_ref.len as usize).min(scratch.len());
1601                let start = body_ref.offset as usize;
1602                scratch[..len].copy_from_slice(&self.arena[start..start + len]);
1603                if self.fill_gap_with_edit(envelope, key, original_id, &scratch[..len]) {
1604                    self.pool.close_slot(slot_index);
1605                    return;
1606                }
1607            }
1608            let edit_handle = if late { self.alloc_handle() } else { handle };
1609            let kind = if body_ref.len == 0 {
1610                MutationKind::Delete {
1611                    conversation: key.conversation,
1612                    original,
1613                }
1614            } else {
1615                MutationKind::Edit {
1616                    conversation: key.conversation,
1617                    original,
1618                    body: body_ref,
1619                }
1620            };
1621            self.emit_mutation(edit_handle, kind);
1622            self.pool.close_slot(slot_index);
1623            return;
1624        }
1625        if is_edit {
1626            // Fragmented edit still incomplete: not displayed until whole.
1627            return;
1628        }
1629
1630        let status = if complete {
1631            CompletionStatus::Complete
1632        } else {
1633            CompletionStatus::Partial {
1634                present,
1635                count,
1636                finalized: false,
1637            }
1638        };
1639        let body_ref = self.render_to_arena(slot_index, complete);
1640        // Notify exactly once, when the message becomes complete (the notify
1641        // deadline in `tick` covers messages that stall incomplete).
1642        let notify = complete && !notified;
1643        if notify && let Some(slot) = self.pool.slots[slot_index].as_mut() {
1644            slot.notified = true;
1645        }
1646
1647        if !announced {
1648            let slot = self.pool.slots[slot_index].as_mut().expect("occupied");
1649            slot.announced = true;
1650            let meta = slot.meta;
1651            let message_type = meta.message_type();
1652            let sender_handle = content
1653                .sender_handle
1654                .and_then(|text| self.arena_store(text));
1655            let regarding = meta
1656                .regarding
1657                .map(|r| self.resolved_from_wire(key.conversation, Some(key.sender), r));
1658            self.emit_mutation(
1659                handle,
1660                MutationKind::Insert {
1661                    conversation: envelope.conversation,
1662                    sender: envelope.sender,
1663                    direction: Direction::Inbound,
1664                    message_type,
1665                    wire_id: Some(id),
1666                    epoch: self.inbound.get(&key).map(|s| s.epoch).unwrap_or(0),
1667                    client_token: None,
1668                    sender_handle,
1669                    regarding,
1670                    bg_color: content.bg_color,
1671                    text_color: content.text_color,
1672                    body: body_ref,
1673                    status,
1674                    presence: Presence::Present,
1675                    late,
1676                    notify,
1677                },
1678            );
1679            if let Some(stream) = self.inbound.get_mut(&key) {
1680                stream.refs.record(id, handle);
1681            }
1682        } else {
1683            self.emit_mutation(
1684                handle,
1685                MutationKind::UpdateBody {
1686                    body: body_ref,
1687                    status,
1688                    late: false,
1689                    notify,
1690                },
1691            );
1692        }
1693
1694        if complete {
1695            self.pool.close_slot(slot_index);
1696            let was_repairing = self.inbound.get_mut(&key).is_some_and(|stream| {
1697                let repairing = stream
1698                    .pending
1699                    .iter()
1700                    .any(|pending| pending.message_id == id && pending.attempts > 0);
1701                stream.cancel_pending(id, None);
1702                repairing
1703            });
1704            if was_repairing {
1705                self.push_output(Output::Event(Event::RepairFinished {
1706                    conversation: key.conversation,
1707                    sender: key.sender,
1708                    message_id: id,
1709                    outcome: RepairOutcome::Repaired,
1710                }));
1711            }
1712        }
1713        let _ = now_ms;
1714    }
1715
1716    #[allow(clippy::too_many_arguments)]
1717    fn insert_content(
1718        &mut self,
1719        envelope: &Envelope,
1720        content: &validate::ContentMessage<'_>,
1721        wire_id: Option<u8>,
1722        status: CompletionStatus,
1723        late: bool,
1724        notify: bool,
1725        reuse: Option<MessageHandle>,
1726        now_ms: u64,
1727    ) -> MessageHandle {
1728        let handle = reuse.unwrap_or_else(|| self.alloc_handle());
1729        let body = core::str::from_utf8(content.body).unwrap_or("");
1730        let body_ref = self
1731            .arena_store(body)
1732            .unwrap_or(BodyRef { offset: 0, len: 0 });
1733        let sender_handle = content
1734            .sender_handle
1735            .and_then(|handle_text| self.arena_store(handle_text));
1736        let regarding = content
1737            .regarding
1738            .map(|r| self.resolved_from_wire(envelope.conversation, Some(envelope.sender), r));
1739        let epoch = self
1740            .inbound
1741            .get(&StreamKey {
1742                conversation: envelope.conversation,
1743                sender: envelope.sender,
1744            })
1745            .map(|stream| stream.epoch)
1746            .unwrap_or(0);
1747        self.emit_mutation(
1748            handle,
1749            MutationKind::Insert {
1750                conversation: envelope.conversation,
1751                sender: envelope.sender,
1752                direction: Direction::Inbound,
1753                message_type: content.message_type,
1754                wire_id,
1755                epoch,
1756                client_token: None,
1757                sender_handle,
1758                regarding,
1759                bg_color: content.bg_color,
1760                text_color: content.text_color,
1761                body: body_ref,
1762                status,
1763                presence: Presence::Present,
1764                late,
1765                notify,
1766            },
1767        );
1768        let _ = now_ms;
1769        handle
1770    }
1771
1772    // ------------------------------------------------------------------
1773    // Resend service
1774    // ------------------------------------------------------------------
1775
1776    fn receive_resend_request(
1777        &mut self,
1778        envelope: &Envelope,
1779        sequence: MessageSequence,
1780        channel_group: bool,
1781        now_ms: u64,
1782    ) {
1783        // The requester must be an individually attributable peer.
1784        let SenderScope::Peer(requester) = envelope.sender else {
1785            self.push_output(Output::Diagnostic(Diagnostic::UnattributableResend));
1786            return;
1787        };
1788        // Select the archive stream from the arrival path and flag.
1789        let conversation = if channel_group {
1790            match envelope.conversation {
1791                ConversationKey::ChannelDirect { channel, .. } => {
1792                    ConversationKey::ChannelGroup { channel }
1793                }
1794                _ => {
1795                    self.push_output(Output::Diagnostic(Diagnostic::UnattributableResend));
1796                    return;
1797                }
1798            }
1799        } else {
1800            envelope.conversation
1801        };
1802
1803        if self.coalesce.recently_answered(
1804            &conversation,
1805            &sequence,
1806            now_ms,
1807            self.config.coalesce_window_ms,
1808        ) {
1809            self.push_output(Output::Diagnostic(Diagnostic::CoalescedResend {
1810                message_id: sequence.message_id,
1811            }));
1812            return;
1813        }
1814
1815        // The requested frame is still in flight on this node's own radio —
1816        // queued behind earlier frames or awaiting its delivery report. On a
1817        // slow serialized link the requester's patience can lapse before the
1818        // original arrives; answering now would duplicate the frame on air
1819        // and delay everything queued behind it. A genuinely lost frame
1820        // leaves `in_flight` with its failure report, after which requests
1821        // are served normally. Only meaningful on platforms that report
1822        // transport progress; without reports, emission tells us nothing
1823        // about whether the frame is still queued.
1824        let requested_fragment = sequence.fragment.map(|fragment| fragment.index);
1825        if self.saw_transmit_report
1826            && self.in_flight.iter().any(|frame| {
1827                frame.archive.is_some_and(|archive| {
1828                    archive.conversation == conversation
1829                        && archive.message_id == sequence.message_id
1830                        && archive.fragment == requested_fragment
1831                })
1832            })
1833        {
1834            self.push_output(Output::Diagnostic(Diagnostic::CoalescedResend {
1835                message_id: sequence.message_id,
1836            }));
1837            return;
1838        }
1839
1840        // A lookup for the same frame is already outstanding: one response
1841        // will serve both requesters.
1842        if self
1843            .lookups
1844            .iter()
1845            .any(|lookup| lookup.conversation == conversation && lookup.sequence == sequence)
1846        {
1847            self.push_output(Output::Diagnostic(Diagnostic::CoalescedResend {
1848                message_id: sequence.message_id,
1849            }));
1850            return;
1851        }
1852
1853        let request_id = self.next_request;
1854        self.next_request = self.next_request.wrapping_add(1);
1855        if self.lookups.is_full() {
1856            self.lookups.remove(0);
1857        }
1858        let _ = self.lookups.push(PendingLookup {
1859            request_id,
1860            conversation,
1861            requester,
1862            sequence,
1863        });
1864        self.push_output(Output::LookupOutbound {
1865            request_id,
1866            conversation,
1867            sequence,
1868        });
1869    }
1870
1871    fn receive_unavailable(&mut self, envelope: &Envelope, sequence: MessageSequence, now_ms: u64) {
1872        let key = StreamKey {
1873            conversation: envelope.conversation,
1874            sender: envelope.sender,
1875        };
1876        let id = sequence.message_id;
1877        let fragment = sequence.fragment.map(|fragment| fragment.index);
1878
1879        // A whole-message gap placeholder (no reassembly slot) reserved for
1880        // this ID becomes a permanent loss marker below.
1881        let placeholder = self.inbound.get(&key).and_then(|stream| {
1882            stream
1883                .pending
1884                .iter()
1885                .find(|pending| pending.message_id == id && pending.fragment.is_none())
1886                .and_then(|pending| pending.handle)
1887        });
1888
1889        if let Some(stream) = self.inbound.get_mut(&key) {
1890            stream.cancel_pending(id, fragment);
1891            // The position is accounted for; it no longer counts as a gap.
1892            stream.seen.insert(id);
1893            stream.last_active_ms = now_ms;
1894        }
1895
1896        let epoch = self
1897            .inbound
1898            .get(&key)
1899            .map(|stream| stream.epoch)
1900            .unwrap_or(0);
1901        let slot = self.pool.find_slot(&key, epoch, id);
1902        if fragment.is_none()
1903            && slot.is_none()
1904            && let Some(handle) = placeholder
1905        {
1906            self.flip_placeholder_unavailable(key, id, handle);
1907        }
1908        if let Some(slot_index) = slot {
1909            match fragment {
1910                Some(index) => {
1911                    let slot = self.pool.slots[slot_index].as_mut().expect("occupied");
1912                    let bit = 1u16 << index;
1913                    if slot.present & bit == 0 {
1914                        slot.unavailable |= bit;
1915                    }
1916                    let settled = {
1917                        let slot = self.pool.slots[slot_index].as_ref().expect("occupied");
1918                        slot.is_settled()
1919                    };
1920                    if settled {
1921                        self.finalize_slot(slot_index, now_ms, RepairOutcome::Unavailable, false);
1922                    } else if self.pool.slots[slot_index]
1923                        .as_ref()
1924                        .is_some_and(|slot| slot.announced)
1925                    {
1926                        let body = self.render_to_arena(slot_index, false);
1927                        let slot = self.pool.slots[slot_index].as_ref().expect("occupied");
1928                        let status = CompletionStatus::Partial {
1929                            present: slot.present,
1930                            count: slot.count,
1931                            finalized: false,
1932                        };
1933                        let handle = slot.handle;
1934                        self.emit_mutation(
1935                            handle,
1936                            MutationKind::UpdateBody {
1937                                body,
1938                                status,
1939                                late: false,
1940                                notify: false,
1941                            },
1942                        );
1943                    }
1944                }
1945                None => {
1946                    self.finalize_slot(slot_index, now_ms, RepairOutcome::Unavailable, false);
1947                }
1948            }
1949        }
1950
1951        self.push_output(Output::Event(Event::MessageUnavailable {
1952            conversation: envelope.conversation,
1953            sender: envelope.sender,
1954            message_id: id,
1955            fragment,
1956        }));
1957    }
1958
1959    // ------------------------------------------------------------------
1960    // Timers
1961    // ------------------------------------------------------------------
1962
1963    fn expire_slots(&mut self, now_ms: u64) {
1964        for index in 0..SLOTS {
1965            let expired = self.pool.slots[index]
1966                .as_ref()
1967                .is_some_and(|slot| now_ms >= slot.deadline_ms);
1968            if expired {
1969                self.finalize_slot(index, now_ms, RepairOutcome::Expired, true);
1970            }
1971        }
1972    }
1973
1974    /// Queue repair entries for missing fragments of stalled assemblies.
1975    fn schedule_fragment_repairs(&mut self, now_ms: u64) {
1976        for index in 0..SLOTS {
1977            let Some(slot) = self.pool.slots[index].as_ref() else {
1978                continue;
1979            };
1980            if now_ms < slot.repair_at_ms || slot.is_settled() {
1981                continue;
1982            }
1983            let key = slot.stream;
1984            let id = slot.message_id;
1985            if let Some(stream) = self.inbound.get_mut(&key) {
1986                if stream.collided {
1987                    continue;
1988                }
1989                // A fragmented message advances through missing fragments
1990                // serially. Do not queue an entire missing bitmap at once:
1991                // one request receives its full retry budget before repair
1992                // moves to the next fragment.
1993                if stream
1994                    .pending
1995                    .iter()
1996                    .any(|pending| pending.message_id == id)
1997                {
1998                    continue;
1999                }
2000                let next = self.pool.slots[index]
2001                    .as_ref()
2002                    .and_then(|slot| slot.repairable_missing().next());
2003                if let Some(fragment) = next {
2004                    let _ = stream.pending.push(PendingRepair {
2005                        message_id: id,
2006                        fragment: Some(fragment),
2007                        deadline_ms: now_ms,
2008                        attempts: 0,
2009                        handle: None,
2010                    });
2011                    if let Some(slot) = self.pool.slots[index].as_mut() {
2012                        slot.repair_at_ms = now_ms + self.config.request_retry_ms;
2013                    }
2014                }
2015            }
2016        }
2017    }
2018
2019    fn transmit_due_repairs(&mut self, now_ms: u64) {
2020        let mut budget = self.config.max_requests_per_tick;
2021        let keys: heapless::Vec<StreamKey, 16> = self.inbound.keys().copied().collect();
2022        for key in keys {
2023            if budget == 0 {
2024                break;
2025            }
2026            let Some(stream) = self.inbound.get(&key) else {
2027                continue;
2028            };
2029            if stream.collided
2030                || now_ms.saturating_sub(stream.last_request_ms)
2031                    < self.config.min_request_interval_ms
2032            {
2033                continue;
2034            }
2035            let Some(position) = stream
2036                .pending
2037                .iter()
2038                .position(|pending| now_ms >= pending.deadline_ms)
2039            else {
2040                continue;
2041            };
2042            let pending = stream.pending[position];
2043
2044            // Resolve the request destination.
2045            let destination = match key.conversation {
2046                ConversationKey::Direct { peer } => Destination::Peer(peer),
2047                ConversationKey::Room { room } => Destination::Peer(room),
2048                ConversationKey::ChannelDirect { channel, peer } => {
2049                    Destination::ChannelPeer { channel, peer }
2050                }
2051                ConversationKey::ChannelGroup { channel } => match stream.sender_key {
2052                    Some(peer) => Destination::ChannelPeer { channel, peer },
2053                    None => {
2054                        // Unaddressable: give up on this frame; expiry will
2055                        // finalize any partial render.
2056                        let stream = self.inbound.get_mut(&key).expect("present");
2057                        stream.pending.remove(position);
2058                        self.push_output(Output::Event(Event::RepairFinished {
2059                            conversation: key.conversation,
2060                            sender: key.sender,
2061                            message_id: pending.message_id,
2062                            outcome: RepairOutcome::Unaddressable,
2063                        }));
2064                        if pending.fragment.is_none()
2065                            && let Some(handle) = pending.handle
2066                        {
2067                            self.flip_placeholder_unavailable(key, pending.message_id, handle);
2068                        }
2069                        continue;
2070                    }
2071                },
2072            };
2073            let channel_group = matches!(key.conversation, ConversationKey::ChannelGroup { .. });
2074
2075            // A fragment request needs the slot's fragment count.
2076            let sequence = match pending.fragment {
2077                None => MessageSequence::unfragmented(pending.message_id),
2078                Some(index) => {
2079                    let epoch = stream.epoch;
2080                    let count = self
2081                        .pool
2082                        .find_slot(&key, epoch, pending.message_id)
2083                        .and_then(|slot| self.pool.slots[slot].as_ref())
2084                        .map(|slot| slot.count);
2085                    let Some(count) = count else {
2086                        let stream = self.inbound.get_mut(&key).expect("present");
2087                        stream.pending.remove(position);
2088                        continue;
2089                    };
2090                    MessageSequence {
2091                        message_id: pending.message_id,
2092                        fragment: Some(Fragment { index, count }),
2093                    }
2094                }
2095            };
2096
2097            let request = TextMessage {
2098                message_type: MessageType::ResendRequest,
2099                sequence: Some(sequence),
2100                channel_group_resend: channel_group,
2101                ..TextMessage::basic("")
2102            };
2103            let mut buffer = [0u8; MAX_FRAME];
2104            let Ok(len) = codec::encode(&request, &mut buffer) else {
2105                continue;
2106            };
2107            let mut frame = heapless::Vec::new();
2108            let _ = frame.extend_from_slice(&buffer[..len]);
2109            self.queue_transmit(destination, None, frame, None);
2110            budget -= 1;
2111
2112            if pending.attempts == 0 {
2113                self.push_output(Output::Event(Event::RepairStarted {
2114                    conversation: key.conversation,
2115                    sender: key.sender,
2116                    message_id: pending.message_id,
2117                    fragment: pending.fragment,
2118                }));
2119            }
2120
2121            let max_attempts = self.config.max_repair_attempts;
2122            let retry_ms = self.config.request_retry_ms;
2123            let stream = self.inbound.get_mut(&key).expect("present");
2124            stream.last_request_ms = now_ms;
2125            let entry = &mut stream.pending[position];
2126            entry.attempts += 1;
2127            if entry.attempts >= max_attempts {
2128                let message_id = entry.message_id;
2129                let fragment = entry.fragment;
2130                let placeholder = entry.handle;
2131                stream.pending.remove(position);
2132                if let Some(fragment) = fragment
2133                    && let Some(slot_index) = self.pool.find_slot(&key, stream.epoch, message_id)
2134                    && let Some(slot) = self.pool.slots[slot_index].as_mut()
2135                {
2136                    slot.repair_exhausted |= 1u16 << fragment;
2137                }
2138                self.push_output(Output::Event(Event::RepairFinished {
2139                    conversation: key.conversation,
2140                    sender: key.sender,
2141                    message_id,
2142                    outcome: RepairOutcome::Exhausted,
2143                }));
2144                // A whole-message gap that exhausted its repair budget: turn
2145                // its reserved slot into a permanent loss marker.
2146                if fragment.is_none()
2147                    && let Some(handle) = placeholder
2148                {
2149                    self.flip_placeholder_unavailable(key, message_id, handle);
2150                }
2151            } else {
2152                entry.deadline_ms = now_ms + retry_ms;
2153            }
2154        }
2155    }
2156
2157    /// Finalize a slot: emit its final partial render (when displayable) and
2158    /// release its pages.
2159    fn finalize_slot(
2160        &mut self,
2161        slot_index: usize,
2162        now_ms: u64,
2163        outcome: RepairOutcome,
2164        evicted: bool,
2165    ) {
2166        let Some(slot) = self.pool.slots[slot_index].as_ref() else {
2167            return;
2168        };
2169        let key = slot.stream;
2170        let id = slot.message_id;
2171        let announced = slot.announced;
2172        let handle = slot.handle;
2173        let complete = slot.is_complete();
2174
2175        if announced && !complete {
2176            let body = self.render_to_arena(slot_index, true);
2177            let slot = self.pool.slots[slot_index].as_ref().expect("occupied");
2178            let status = CompletionStatus::Partial {
2179                present: slot.present,
2180                count: slot.count,
2181                finalized: true,
2182            };
2183            self.emit_mutation(
2184                handle,
2185                MutationKind::UpdateBody {
2186                    body,
2187                    status,
2188                    late: false,
2189                    notify: false,
2190                },
2191            );
2192        }
2193        self.pool.close_slot(slot_index);
2194        if evicted {
2195            self.push_output(Output::Diagnostic(Diagnostic::ReassemblyEvicted {
2196                message_id: id,
2197            }));
2198        }
2199        if let Some(stream) = self.inbound.get_mut(&key) {
2200            stream.cancel_pending(id, None);
2201            stream.last_active_ms = now_ms;
2202        }
2203        self.push_output(Output::Event(Event::RepairFinished {
2204            conversation: key.conversation,
2205            sender: key.sender,
2206            message_id: id,
2207            outcome,
2208        }));
2209    }
2210
2211    // ------------------------------------------------------------------
2212    // Outbound encoding
2213    // ------------------------------------------------------------------
2214
2215    fn encode_and_queue(
2216        &mut self,
2217        conversation: ConversationKey,
2218        handle: MessageHandle,
2219        template: &TextMessage<'_>,
2220        body: &[u8],
2221        message_id: u8,
2222    ) -> Result<(), ComposeError> {
2223        let destination = destination_for(&conversation);
2224
2225        // Trial-encode with an empty body to learn the option overhead.
2226        let mut trial = *template;
2227        trial.body = &[];
2228        let mut buffer = [0u8; MAX_FRAME];
2229        let overhead = codec::encode(&trial, &mut buffer).map_err(ComposeError::Encode)?;
2230        let single_budget = MAX_FRAME.saturating_sub(overhead + 1);
2231
2232        let Some(plan) =
2233            FragmentPlan::plan(body.len(), single_budget).map_err(|_| ComposeError::TooLarge)?
2234        else {
2235            let mut message = *template;
2236            message.body = body;
2237            let len = codec::encode(&message, &mut buffer).map_err(ComposeError::Encode)?;
2238            let mut frame = heapless::Vec::new();
2239            let _ = frame.extend_from_slice(&buffer[..len]);
2240            self.queue_transmit(
2241                destination,
2242                Some(ArchiveKey {
2243                    conversation,
2244                    message_id,
2245                    fragment: None,
2246                }),
2247                frame,
2248                Some((handle, None)),
2249            );
2250            return Ok(());
2251        };
2252
2253        for index in 0..plan.count {
2254            let range = plan.range(index);
2255            let mut message = if index == 0 {
2256                *template
2257            } else {
2258                // Continuation fragments carry only sequence metadata.
2259                let mut continuation = TextMessage::basic("");
2260                continuation.sequence = template.sequence;
2261                continuation
2262            };
2263            message.sequence = Some(MessageSequence {
2264                message_id,
2265                fragment: Some(Fragment {
2266                    index,
2267                    count: plan.count,
2268                }),
2269            });
2270            message.body = &body[range];
2271            let len = codec::encode(&message, &mut buffer).map_err(ComposeError::Encode)?;
2272            if len > MAX_FRAME {
2273                return Err(ComposeError::TooLarge);
2274            }
2275            let mut frame = heapless::Vec::new();
2276            let _ = frame.extend_from_slice(&buffer[..len]);
2277            self.queue_transmit(
2278                destination,
2279                Some(ArchiveKey {
2280                    conversation,
2281                    message_id,
2282                    fragment: Some(index),
2283                }),
2284                frame,
2285                Some((handle, Some(index))),
2286            );
2287        }
2288        Ok(())
2289    }
2290
2291    /// Re-encode `body` under the original message ID and emit it as
2292    /// archive-only material (never transmitted): the resend service will
2293    /// serve this in place of the superseded original. The replacement is a
2294    /// plain content frame — the option set the original carried is not
2295    /// retained by the engine, and a requester that missed the original only
2296    /// needs its current content at its sequence position. Best-effort: an
2297    /// encode failure leaves the ID's archive empty (the preceding
2298    /// [`Output::DeleteArchive`] already retired the original), which the
2299    /// resend service answers as Message Unavailable — never stale content.
2300    fn archive_replacement(&mut self, conversation: ConversationKey, message_id: u8, body: &[u8]) {
2301        let mut template = TextMessage::basic("");
2302        template.sequence = Some(MessageSequence::unfragmented(message_id));
2303
2304        let mut buffer = [0u8; MAX_FRAME];
2305        let Ok(overhead) = codec::encode(&template, &mut buffer) else {
2306            return;
2307        };
2308        let single_budget = MAX_FRAME.saturating_sub(overhead + 1);
2309        let Ok(plan) = FragmentPlan::plan(body.len(), single_budget) else {
2310            return;
2311        };
2312
2313        let Some(plan) = plan else {
2314            let mut message = template;
2315            message.body = body;
2316            let Ok(len) = codec::encode(&message, &mut buffer) else {
2317                return;
2318            };
2319            let mut frame = heapless::Vec::new();
2320            let _ = frame.extend_from_slice(&buffer[..len]);
2321            self.push_output(Output::StoreArchive {
2322                key: ArchiveKey {
2323                    conversation,
2324                    message_id,
2325                    fragment: None,
2326                },
2327                payload: frame,
2328            });
2329            return;
2330        };
2331
2332        for index in 0..plan.count {
2333            let range = plan.range(index);
2334            let mut message = TextMessage::basic("");
2335            message.sequence = Some(MessageSequence {
2336                message_id,
2337                fragment: Some(Fragment {
2338                    index,
2339                    count: plan.count,
2340                }),
2341            });
2342            message.body = &body[range];
2343            let Ok(len) = codec::encode(&message, &mut buffer) else {
2344                return;
2345            };
2346            let mut frame = heapless::Vec::new();
2347            let _ = frame.extend_from_slice(&buffer[..len]);
2348            self.push_output(Output::StoreArchive {
2349                key: ArchiveKey {
2350                    conversation,
2351                    message_id,
2352                    fragment: Some(index),
2353                },
2354                payload: frame,
2355            });
2356        }
2357    }
2358
2359    fn queue_transmit(
2360        &mut self,
2361        destination: Destination,
2362        archive: Option<ArchiveKey>,
2363        payload: heapless::Vec<u8, MAX_FRAME>,
2364        track: Option<(MessageHandle, Option<u8>)>,
2365    ) -> u32 {
2366        let transmission_id = self.next_transmission;
2367        self.next_transmission = self.next_transmission.wrapping_add(1);
2368        if let Some((handle, fragment)) = track {
2369            if self.in_flight.is_full() {
2370                self.in_flight.remove(0);
2371            }
2372            let _ = self.in_flight.push(InFlightFrame {
2373                transmission_id,
2374                handle,
2375                fragment,
2376                archive,
2377                expects_ack: matches!(destination, Destination::Peer(_)),
2378            });
2379        }
2380        self.push_output(Output::Transmit(Transmission {
2381            transmission_id,
2382            destination,
2383            archive,
2384            payload,
2385        }));
2386        transmission_id
2387    }
2388
2389    // ------------------------------------------------------------------
2390    // Reference resolution
2391    // ------------------------------------------------------------------
2392
2393    /// Build the wire Regarding form for a locally known message handle.
2394    fn wire_reference_for(
2395        &self,
2396        conversation: ConversationKey,
2397        handle: MessageHandle,
2398    ) -> Option<Regarding> {
2399        let multicast = conversation.uses_multicast_references();
2400        if let Some(stream) = self.outbound.get(&conversation)
2401            && let Some(id) = stream.refs.lookup_handle(handle)
2402        {
2403            return Some(if multicast {
2404                Regarding::Multicast {
2405                    message_id: id,
2406                    source_prefix: umsh_core::NodeHint([
2407                        self.local_key.0[0],
2408                        self.local_key.0[1],
2409                        self.local_key.0[2],
2410                    ]),
2411                }
2412            } else {
2413                Regarding::Unicast { message_id: id }
2414            });
2415        }
2416        for (key, stream) in self.inbound.iter() {
2417            if key.conversation != conversation {
2418                continue;
2419            }
2420            if let Some(id) = stream.refs.lookup_handle(handle) {
2421                return Some(if multicast {
2422                    let prefix = key.sender.hint()?;
2423                    Regarding::Multicast {
2424                        message_id: id,
2425                        source_prefix: prefix,
2426                    }
2427                } else {
2428                    Regarding::Unicast { message_id: id }
2429                });
2430            }
2431        }
2432        None
2433    }
2434
2435    /// Resolve a compose-time regarding target into the wire form to send and
2436    /// the reference to export on the transcript mutation.
2437    fn resolve_regarding(
2438        &self,
2439        conversation: ConversationKey,
2440        target: RegardingRef,
2441    ) -> Result<(Regarding, ResolvedRef), ComposeError> {
2442        let (message_id, direction, sender_hint, epoch) = match target {
2443            RegardingRef::Handle(handle) => {
2444                let wire = self
2445                    .wire_reference_for(conversation, handle)
2446                    .ok_or(ComposeError::UnknownRegarding)?;
2447                return Ok((wire, ResolvedRef::Handle(handle)));
2448            }
2449            RegardingRef::Wire {
2450                message_id,
2451                direction,
2452                sender_hint,
2453                epoch,
2454            } => (message_id, direction, sender_hint, epoch),
2455        };
2456
2457        let sender = regarding_sender(conversation, direction, sender_hint)
2458            .ok_or(ComposeError::UnknownRegarding)?;
2459        match direction {
2460            Direction::Outbound => {
2461                let stream = self
2462                    .outbound
2463                    .get(&conversation)
2464                    .ok_or(ComposeError::UnknownRegarding)?;
2465                // A reference that predates a Sequence Reset names an ID the
2466                // receiver has already discarded as a target.
2467                if stream.announce_reset || epoch.is_some_and(|epoch| stream.epoch != epoch) {
2468                    return Err(ComposeError::UnknownRegarding);
2469                }
2470                // The ID must lie in the already allocated serial half-space.
2471                let delta = stream.next_id.wrapping_sub(message_id);
2472                if delta == 0 || delta > 128 {
2473                    return Err(ComposeError::UnknownRegarding);
2474                }
2475            }
2476            Direction::Inbound => {
2477                let live = self.inbound.get(&StreamKey {
2478                    conversation,
2479                    sender,
2480                });
2481                if let Some(stream) = live
2482                    && epoch.is_some_and(|epoch| stream.epoch != epoch)
2483                {
2484                    return Err(ComposeError::UnknownRegarding);
2485                }
2486            }
2487        }
2488
2489        let wire = if conversation.uses_multicast_references() {
2490            let source_prefix = match direction {
2491                Direction::Outbound => umsh_core::NodeHint([
2492                    self.local_key.0[0],
2493                    self.local_key.0[1],
2494                    self.local_key.0[2],
2495                ]),
2496                Direction::Inbound => sender.hint().ok_or(ComposeError::UnknownRegarding)?,
2497            };
2498            Regarding::Multicast {
2499                message_id,
2500                source_prefix,
2501            }
2502        } else {
2503            Regarding::Unicast { message_id }
2504        };
2505        // No live handle backs a wire-addressed target; the platform matches
2506        // the exported reference against its own persisted rows.
2507        Ok((
2508            wire,
2509            ResolvedRef::Unresolved(crate::model::WireRef::SenderScoped { sender, message_id }),
2510        ))
2511    }
2512
2513    /// Resolve a received wire reference to a stable handle when unambiguous.
2514    fn resolved_from_wire(
2515        &self,
2516        conversation: ConversationKey,
2517        sender: Option<SenderScope>,
2518        regarding: Regarding,
2519    ) -> ResolvedRef {
2520        match regarding {
2521            Regarding::Multicast {
2522                message_id,
2523                source_prefix,
2524            } => {
2525                let local_prefix = umsh_core::NodeHint([
2526                    self.local_key.0[0],
2527                    self.local_key.0[1],
2528                    self.local_key.0[2],
2529                ]);
2530                if source_prefix == local_prefix
2531                    && let Some(stream) = self.outbound.get(&conversation)
2532                {
2533                    let message_id = stream.edit_refs.resolve(message_id);
2534                    if let Some(handle) = stream.refs.lookup(message_id) {
2535                        return ResolvedRef::Handle(handle);
2536                    }
2537                }
2538                let key = StreamKey {
2539                    conversation,
2540                    sender: SenderScope::ClaimedMember(source_prefix),
2541                };
2542                // A reference naming an edit stands for the message it
2543                // replaced; carry the collapsed ID into the fallback too, so
2544                // the platform matches the row it actually stored.
2545                let message_id = self
2546                    .inbound
2547                    .get(&key)
2548                    .map(|stream| stream.edit_refs.resolve(message_id))
2549                    .unwrap_or(message_id);
2550                let unresolved = ResolvedRef::Unresolved(crate::model::WireRef::SenderScoped {
2551                    sender: SenderScope::ClaimedMember(source_prefix),
2552                    message_id,
2553                });
2554                match self.inbound.get(&key) {
2555                    Some(stream) if !stream.collided => stream
2556                        .refs
2557                        .lookup(message_id)
2558                        .map(ResolvedRef::Handle)
2559                        .unwrap_or(unresolved),
2560                    _ => unresolved,
2561                }
2562            }
2563            Regarding::Unicast { message_id } => {
2564                // In a one-to-one conversation the reference may target
2565                // either party's stream; resolve only when unambiguous.
2566                let inbound_key = sender.map(|sender| StreamKey {
2567                    conversation,
2568                    sender,
2569                });
2570                let inbound_stream = inbound_key.and_then(|key| self.inbound.get(&key));
2571                let outbound_stream = self.outbound.get(&conversation);
2572                // Either side may have edited the target; collapse through
2573                // whichever stream knows the ID as an edit of its own.
2574                let message_id = inbound_stream
2575                    .map(|stream| stream.edit_refs.resolve(message_id))
2576                    .filter(|resolved| *resolved != message_id)
2577                    .or_else(|| outbound_stream.map(|stream| stream.edit_refs.resolve(message_id)))
2578                    .unwrap_or(message_id);
2579                let inbound = inbound_stream.and_then(|stream| stream.refs.lookup(message_id));
2580                let outbound = outbound_stream.and_then(|stream| stream.refs.lookup(message_id));
2581                match (inbound, outbound) {
2582                    (Some(handle), None) | (None, Some(handle)) => ResolvedRef::Handle(handle),
2583                    _ => ResolvedRef::Unresolved(match sender {
2584                        Some(sender) => crate::model::WireRef::SenderScoped { sender, message_id },
2585                        None => crate::model::WireRef::RoomCanonical { message_id },
2586                    }),
2587                }
2588            }
2589        }
2590    }
2591
2592    // ------------------------------------------------------------------
2593    // Infrastructure
2594    // ------------------------------------------------------------------
2595
2596    fn ensure_outbound(&mut self, conversation: ConversationKey, now_ms: u64) {
2597        if self.outbound.contains_key(&conversation) {
2598            return;
2599        }
2600        if self.outbound.len() == self.outbound.capacity()
2601            && let Some(oldest) = self
2602                .outbound
2603                .iter()
2604                .min_by_key(|(_, stream)| stream.last_active_ms)
2605                .map(|(key, _)| *key)
2606        {
2607            // Demote the evicted stream's continuity to the cold stash so
2608            // reactivation resumes its sequence instead of resetting.
2609            if let Some(stream) = self.outbound.remove(&oldest)
2610                && !stream.announce_reset
2611            {
2612                self.stash_checkpoint(oldest, stream.next_id, stream.epoch);
2613            }
2614            self.push_output(Output::Diagnostic(Diagnostic::StreamEvicted));
2615        }
2616        let cold = self
2617            .cold_checkpoints
2618            .iter()
2619            .position(|(key, _, _)| *key == conversation);
2620        let stream = match cold {
2621            Some(position) => {
2622                let (_, next_id, epoch) = self.cold_checkpoints.remove(position);
2623                OutboundStream {
2624                    next_id,
2625                    epoch,
2626                    announce_reset: false,
2627                    refs: Default::default(),
2628                    edit_refs: Default::default(),
2629                    last_active_ms: now_ms,
2630                }
2631            }
2632            None => OutboundStream::fresh(now_ms),
2633        };
2634        let _ = self.outbound.insert(conversation, stream);
2635    }
2636
2637    /// Record `(next_id, epoch)` continuity for an inactive conversation,
2638    /// displacing the oldest entry when the stash is full.
2639    fn stash_checkpoint(&mut self, conversation: ConversationKey, next_id: u8, epoch: u16) {
2640        self.cold_checkpoints
2641            .retain(|(key, _, _)| *key != conversation);
2642        if self.cold_checkpoints.is_full() {
2643            self.cold_checkpoints.remove(0);
2644        }
2645        let _ = self.cold_checkpoints.push((conversation, next_id, epoch));
2646    }
2647
2648    fn ensure_inbound(&mut self, key: StreamKey, now_ms: u64) {
2649        if self.inbound.contains_key(&key) {
2650            return;
2651        }
2652        if self.inbound.len() == self.inbound.capacity()
2653            && let Some(oldest) = self
2654                .inbound
2655                .iter()
2656                .min_by_key(|(_, stream)| stream.last_active_ms)
2657                .map(|(key, _)| *key)
2658        {
2659            self.pool.drop_stream(&oldest);
2660            self.inbound.remove(&oldest);
2661            self.push_output(Output::Diagnostic(Diagnostic::StreamEvicted));
2662        }
2663        let _ = self.inbound.insert(key, InboundStream::new(now_ms));
2664    }
2665
2666    fn alloc_handle(&mut self) -> MessageHandle {
2667        let handle = MessageHandle(self.next_handle);
2668        self.next_handle = self.next_handle.wrapping_add(1);
2669        handle
2670    }
2671
2672    /// Reserve an ordered transcript slot for a detected sequence gap: emit a
2673    /// `GapPending` placeholder (empty body, spinner on the host) and register
2674    /// its handle so the backfilled frame fills the same slot in place.
2675    fn emit_gap_placeholder(&mut self, key: StreamKey, id: u8, epoch: u16) -> MessageHandle {
2676        let handle = self.alloc_handle();
2677        self.emit_mutation(
2678            handle,
2679            MutationKind::Insert {
2680                conversation: key.conversation,
2681                sender: key.sender,
2682                direction: Direction::Inbound,
2683                message_type: MessageType::Basic,
2684                wire_id: Some(id),
2685                epoch,
2686                client_token: None,
2687                sender_handle: None,
2688                regarding: None,
2689                bg_color: None,
2690                text_color: None,
2691                body: BodyRef { offset: 0, len: 0 },
2692                status: CompletionStatus::Complete,
2693                presence: Presence::GapPending,
2694                late: false,
2695                notify: false,
2696            },
2697        );
2698        if let Some(stream) = self.inbound.get_mut(&key) {
2699            stream.refs.record(id, handle);
2700        }
2701        handle
2702    }
2703
2704    /// An edit arrived whose target is a still-missing message with a
2705    /// reserved gap slot. The edit *is* that slot's current content: fill the
2706    /// placeholder with it (or remove the placeholder for a delete), cancel
2707    /// the pending repair, and account for the original ID so the superseded
2708    /// original — should it still arrive — is dropped as a duplicate instead
2709    /// of overwriting the newer content. Returns whether a slot was filled.
2710    fn fill_gap_with_edit(
2711        &mut self,
2712        envelope: &Envelope,
2713        key: StreamKey,
2714        original_id: u8,
2715        body: &[u8],
2716    ) -> bool {
2717        let Some(placeholder) = self.inbound.get(&key).and_then(|stream| {
2718            stream
2719                .pending
2720                .iter()
2721                .find(|pending| pending.message_id == original_id && pending.fragment.is_none())
2722                .and_then(|pending| pending.handle)
2723        }) else {
2724            return false;
2725        };
2726
2727        if let Some(stream) = self.inbound.get_mut(&key) {
2728            stream.cancel_pending(original_id, None);
2729            stream.seen.insert(original_id);
2730        }
2731        self.push_output(Output::Event(Event::RepairFinished {
2732            conversation: key.conversation,
2733            sender: key.sender,
2734            message_id: original_id,
2735            outcome: RepairOutcome::Repaired,
2736        }));
2737
2738        if body.is_empty() {
2739            // The missing message was deleted; its slot simply goes away.
2740            self.emit_mutation(
2741                placeholder,
2742                MutationKind::Delete {
2743                    conversation: key.conversation,
2744                    original: ResolvedRef::Handle(placeholder),
2745                },
2746            );
2747            return true;
2748        }
2749
2750        let text = core::str::from_utf8(body).unwrap_or("");
2751        let body_ref = self
2752            .arena_store(text)
2753            .unwrap_or(BodyRef { offset: 0, len: 0 });
2754        let epoch = self
2755            .inbound
2756            .get(&key)
2757            .map(|stream| stream.epoch)
2758            .unwrap_or(0);
2759        self.emit_mutation(
2760            placeholder,
2761            MutationKind::Insert {
2762                conversation: envelope.conversation,
2763                sender: envelope.sender,
2764                direction: Direction::Inbound,
2765                message_type: MessageType::Basic,
2766                wire_id: Some(original_id),
2767                epoch,
2768                client_token: None,
2769                sender_handle: None,
2770                regarding: None,
2771                bg_color: None,
2772                text_color: None,
2773                body: body_ref,
2774                status: CompletionStatus::Complete,
2775                presence: Presence::Present,
2776                late: true,
2777                notify: true,
2778            },
2779        );
2780        // The slot shows edited content; mark it as such.
2781        self.emit_mutation(
2782            placeholder,
2783            MutationKind::Edit {
2784                conversation: key.conversation,
2785                original: ResolvedRef::Handle(placeholder),
2786                body: body_ref,
2787            },
2788        );
2789        true
2790    }
2791
2792    /// Flip a still-outstanding gap placeholder to `Unavailable`: the repair
2793    /// was exhausted, expired, disclaimed, or abandoned. The row stays in
2794    /// place as a visible loss marker rather than silently vanishing.
2795    fn flip_placeholder_unavailable(&mut self, key: StreamKey, id: u8, handle: MessageHandle) {
2796        let epoch = self
2797            .inbound
2798            .get(&key)
2799            .map(|stream| stream.epoch)
2800            .unwrap_or(0);
2801        self.emit_mutation(
2802            handle,
2803            MutationKind::Insert {
2804                conversation: key.conversation,
2805                sender: key.sender,
2806                direction: Direction::Inbound,
2807                message_type: MessageType::Basic,
2808                wire_id: Some(id),
2809                epoch,
2810                client_token: None,
2811                sender_handle: None,
2812                regarding: None,
2813                bg_color: None,
2814                text_color: None,
2815                body: BodyRef { offset: 0, len: 0 },
2816                status: CompletionStatus::Complete,
2817                presence: Presence::Unavailable,
2818                late: false,
2819                notify: false,
2820            },
2821        );
2822        if let Some(stream) = self.inbound.get_mut(&key) {
2823            stream.refs.retire(id);
2824        }
2825    }
2826
2827    fn emit_mutation(&mut self, handle: MessageHandle, kind: MutationKind) {
2828        let revision = self.revision;
2829        self.revision = self.revision.wrapping_add(1);
2830        self.push_output(Output::StoreMessage(MessageMutation {
2831            handle,
2832            revision,
2833            kind,
2834        }));
2835    }
2836
2837    fn push_output(&mut self, output: Output) {
2838        if self.outputs.push_back(output).is_err() {
2839            self.lost_outputs = self.lost_outputs.wrapping_add(1);
2840        }
2841    }
2842
2843    fn arena_store(&mut self, text: &str) -> Option<BodyRef> {
2844        let bytes = text.as_bytes();
2845        if self.arena_used + bytes.len() > ARENA_SIZE || bytes.len() > u16::MAX as usize {
2846            self.push_output(Output::Diagnostic(Diagnostic::OutputOverflow));
2847            return None;
2848        }
2849        let offset = self.arena_used;
2850        self.arena[offset..offset + bytes.len()].copy_from_slice(bytes);
2851        self.arena_used += bytes.len();
2852        Some(BodyRef {
2853            offset: offset as u16,
2854            len: bytes.len() as u16,
2855        })
2856    }
2857
2858    /// Render a slot into the arena, returning the body reference.
2859    fn render_to_arena(&mut self, slot_index: usize, final_render: bool) -> BodyRef {
2860        let mut scratch = [0u8; REASSEMBLED_BODY_MAX + 64];
2861        let result = fragment::render_slot(
2862            &self.pool,
2863            slot_index,
2864            &self.config.sentinels,
2865            final_render,
2866            &mut scratch,
2867        );
2868        if result.complete && result.had_invalid {
2869            // The spec validates UTF-8 only once every fragment is present;
2870            // a complete body that fails is rendered lossily and reported.
2871            let message_id = self.pool.slots[slot_index]
2872                .as_ref()
2873                .expect("occupied")
2874                .message_id;
2875            self.push_output(Output::Diagnostic(Diagnostic::ReassembledInvalidUtf8 {
2876                message_id,
2877            }));
2878        }
2879        let text = core::str::from_utf8(&scratch[..result.len]).unwrap_or("");
2880        self.arena_store(text)
2881            .unwrap_or(BodyRef { offset: 0, len: 0 })
2882    }
2883}
2884
2885/// Delivery mode of a conversation, used for original sends and for resend
2886/// responses (which return on the conversation's mode, not the request's
2887/// arrival path).
2888pub fn destination_for(conversation: &ConversationKey) -> Destination {
2889    match conversation {
2890        ConversationKey::Direct { peer } => Destination::Peer(*peer),
2891        ConversationKey::Room { room } => Destination::Peer(*room),
2892        ConversationKey::ChannelGroup { channel } => Destination::Channel(*channel),
2893        ConversationKey::ChannelDirect { channel, peer } => Destination::ChannelPeer {
2894            channel: *channel,
2895            peer: *peer,
2896        },
2897    }
2898}