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            // The edit supersedes the original outright, resolved or not. A
1280            // late copy of that original — a repeater can still be retrying
1281            // frames the sender abandoned — must render as a duplicate, not
1282            // as a second message beside the edited one.
1283            if let Some(stream) = self.inbound.get_mut(&key) {
1284                stream.seen.insert(original_id);
1285            }
1286            // Edits and deletes target the sender's own stream.
1287            let original = self
1288                .inbound
1289                .get(&key)
1290                .and_then(|stream| stream.refs.lookup(original_id))
1291                .map(ResolvedRef::Handle)
1292                .unwrap_or(ResolvedRef::Unresolved(
1293                    crate::model::WireRef::SenderScoped {
1294                        sender: envelope.sender,
1295                        message_id: original_id,
1296                    },
1297                ));
1298            let handle = self.alloc_handle();
1299            let kind = if content.body.is_empty() {
1300                MutationKind::Delete {
1301                    conversation: key.conversation,
1302                    original,
1303                }
1304            } else {
1305                let body = core::str::from_utf8(content.body).unwrap_or("");
1306                let body_ref = self
1307                    .arena_store(body)
1308                    .unwrap_or(BodyRef { offset: 0, len: 0 });
1309                MutationKind::Edit {
1310                    conversation: key.conversation,
1311                    original,
1312                    body: body_ref,
1313                }
1314            };
1315            self.emit_mutation(handle, kind);
1316            return;
1317        }
1318
1319        let late = placeholder.is_some();
1320        let handle = self.insert_content(
1321            envelope,
1322            content,
1323            Some(id),
1324            CompletionStatus::Complete,
1325            late,
1326            true,
1327            placeholder,
1328            now_ms,
1329        );
1330        if let Some(stream) = self.inbound.get_mut(&key) {
1331            stream.refs.record(id, handle);
1332        }
1333    }
1334
1335    fn receive_fragment(
1336        &mut self,
1337        envelope: &Envelope,
1338        content: &validate::ContentMessage<'_>,
1339        key: StreamKey,
1340        id: u8,
1341        fragment: Fragment,
1342        now_ms: u64,
1343    ) {
1344        if fragment.count > FRAGMENT_COUNT_MAX {
1345            // Above the wire maximum: account for the ID, drop the assembly.
1346            self.push_output(Output::Diagnostic(Diagnostic::FragmentCountExceeded {
1347                message_id: id,
1348                count: fragment.count,
1349            }));
1350            return;
1351        }
1352        let epoch = self
1353            .inbound
1354            .get(&key)
1355            .map(|stream| stream.epoch)
1356            .unwrap_or(0);
1357
1358        let slot_index = match self.pool.find_slot(&key, epoch, id) {
1359            Some(index) => {
1360                let slot = self.pool.slots[index].as_ref().expect("occupied");
1361                if slot.count != fragment.count {
1362                    self.push_output(Output::Diagnostic(Diagnostic::FragmentCountMismatch {
1363                        message_id: id,
1364                    }));
1365                    return;
1366                }
1367                index
1368            }
1369            None => {
1370                // A handle already registered for this wire ID is a gap
1371                // placeholder reserved earlier; reuse it so the reassembly
1372                // fills that ordered slot in place.
1373                let placeholder = self
1374                    .inbound
1375                    .get(&key)
1376                    .and_then(|stream| stream.refs.lookup(id));
1377                let handle = placeholder.unwrap_or_else(|| self.alloc_handle());
1378                let mut slot = empty_slot(key, epoch, id, fragment.count, handle, now_ms);
1379                slot.late = placeholder.is_some();
1380                slot.deadline_ms = now_ms + self.config.reassembly_ttl_ms;
1381                let group = matches!(key.conversation, ConversationKey::ChannelGroup { .. });
1382                let jitter = if group {
1383                    self.jitter.jitter_ms(self.config.group_jitter_ms)
1384                } else {
1385                    0
1386                };
1387                slot.repair_at_ms = now_ms + self.config.fragment_grace_ms + jitter;
1388                match self.pool.open_slot(slot.clone()) {
1389                    Some(index) => index,
1390                    None => {
1391                        // Evict the oldest assembly to make room.
1392                        if let Some(oldest) = self.pool.oldest_slot() {
1393                            self.finalize_slot(oldest, now_ms, RepairOutcome::Expired, true);
1394                        }
1395                        match self.pool.open_slot(slot) {
1396                            Some(index) => index,
1397                            None => {
1398                                self.push_output(Output::Diagnostic(
1399                                    Diagnostic::ReassemblyEvicted { message_id: id },
1400                                ));
1401                                return;
1402                            }
1403                        }
1404                    }
1405                }
1406            }
1407        };
1408
1409        // Fragment zero carries the message-level metadata, which applies to
1410        // the entire reassembled message. Captured first-arrival-wins and
1411        // before storage, so even an oversized fragment zero contributes its
1412        // valid, authenticated options.
1413        if fragment.index == 0 {
1414            let slot = self.pool.slots[slot_index].as_mut().expect("occupied");
1415            if !slot.have_meta {
1416                slot.meta = fragment::FirstMeta {
1417                    message_type_byte: content.message_type.to_byte(),
1418                    regarding: content.regarding,
1419                    editing: content.editing,
1420                };
1421                slot.have_meta = true;
1422            }
1423        }
1424
1425        if content.body.len() > FRAGMENT_BODY_MAX {
1426            // Syntactically valid but beyond this receiver's storage (the
1427            // sender violated the wire maximum). Salvage the rest of the
1428            // message: mark just this fragment unavailable — a resend would
1429            // return the same oversized bytes — and let the assembly proceed
1430            // for every fragment we can hold.
1431            self.push_output(Output::Diagnostic(Diagnostic::OversizedFragment {
1432                message_id: id,
1433                fragment: fragment.index,
1434            }));
1435            let slot = self.pool.slots[slot_index].as_mut().expect("occupied");
1436            let bit = 1u16 << fragment.index;
1437            if slot.present & bit == 0 {
1438                slot.unavailable |= bit;
1439            }
1440            self.publish_slot(envelope, content, slot_index, now_ms);
1441            if self.pool.slots[slot_index]
1442                .as_ref()
1443                .is_some_and(|slot| slot.is_settled() && !slot.is_complete())
1444            {
1445                self.finalize_slot(slot_index, now_ms, RepairOutcome::Unavailable, false);
1446            }
1447            return;
1448        }
1449
1450        // Store the fragment bytes.
1451        let mut outcome = self
1452            .pool
1453            .insert_fragment(slot_index, fragment.index, content.body);
1454        if outcome == InsertOutcome::NoSpace {
1455            // Free pages by evicting the oldest *other* slot, then retry.
1456            let oldest = self.pool.oldest_slot().filter(|index| *index != slot_index);
1457            if let Some(oldest) = oldest {
1458                self.finalize_slot(oldest, now_ms, RepairOutcome::Expired, true);
1459                outcome = self
1460                    .pool
1461                    .insert_fragment(slot_index, fragment.index, content.body);
1462            }
1463        }
1464        match outcome {
1465            InsertOutcome::Stored => {}
1466            InsertOutcome::Duplicate => {
1467                self.push_output(Output::Diagnostic(Diagnostic::DuplicateFragment {
1468                    message_id: id,
1469                    fragment: fragment.index,
1470                }));
1471                return;
1472            }
1473            InsertOutcome::Conflict => {
1474                self.push_output(Output::Diagnostic(Diagnostic::FragmentConflict {
1475                    message_id: id,
1476                    fragment: fragment.index,
1477                }));
1478                return;
1479            }
1480            InsertOutcome::NoSpace => {
1481                self.push_output(Output::Diagnostic(Diagnostic::ReassemblyEvicted {
1482                    message_id: id,
1483                }));
1484                return;
1485            }
1486            InsertOutcome::TooLarge => {
1487                // Validation already rejects oversized bodies; this arm keeps
1488                // the pool guard observable if that ever regresses.
1489                self.push_output(Output::Diagnostic(Diagnostic::OversizedFragment {
1490                    message_id: id,
1491                    fragment: fragment.index,
1492                }));
1493                return;
1494            }
1495        }
1496
1497        // A stored fragment is proof the sender is still delivering: defer
1498        // repair by at least the configured grace, and by twice the observed
1499        // inter-fragment gap when the link is slower than that. Requesting a
1500        // resend of a frame the sender has merely not reached yet duplicates
1501        // it on air and delays the frames behind it — the repair timer must
1502        // only fire once arrivals actually stall.
1503        {
1504            let group = matches!(key.conversation, ConversationKey::ChannelGroup { .. });
1505            let jitter = if group {
1506                self.jitter.jitter_ms(self.config.group_jitter_ms)
1507            } else {
1508                0
1509            };
1510            let grace = self.config.fragment_grace_ms;
1511            let slot = self.pool.slots[slot_index].as_mut().expect("occupied");
1512            let gap = now_ms.saturating_sub(slot.last_fragment_ms);
1513            slot.last_fragment_ms = now_ms;
1514            let holdoff = grace.max(gap.saturating_mul(2));
1515            slot.repair_at_ms = slot.repair_at_ms.max(now_ms + holdoff + jitter);
1516        }
1517
1518        self.publish_slot(envelope, content, slot_index, now_ms);
1519        // A stored fragment can settle a slot that carries unavailable
1520        // marks; nothing further can improve it, so finalize now rather
1521        // than waiting for the reassembly TTL.
1522        if self.pool.slots[slot_index]
1523            .as_ref()
1524            .is_some_and(|slot| slot.is_settled() && !slot.is_complete())
1525        {
1526            self.finalize_slot(slot_index, now_ms, RepairOutcome::Unavailable, false);
1527        }
1528    }
1529
1530    /// Emit the appropriate mutation for a slot's current state, completing
1531    /// it if every fragment is present.
1532    ///
1533    /// `content` is the fragment that triggered this call. The announcing
1534    /// Insert always runs during the call that delivered fragment zero
1535    /// (`have_meta` is set in that same call), so presentation metadata —
1536    /// sender handle and colors — is borrowed from `content` at full
1537    /// fidelity instead of being retained in the slot.
1538    fn publish_slot(
1539        &mut self,
1540        envelope: &Envelope,
1541        content: &validate::ContentMessage<'_>,
1542        slot_index: usize,
1543        now_ms: u64,
1544    ) {
1545        let slot = self.pool.slots[slot_index].as_ref().expect("occupied");
1546        let key = slot.stream;
1547        let handle = slot.handle;
1548        let complete = slot.is_complete();
1549        let have_meta = slot.have_meta;
1550        let is_edit = slot.meta.editing.is_some();
1551        let announced = slot.announced;
1552        let late = slot.late;
1553        let notified = slot.notified;
1554        let id = slot.message_id;
1555        let present = slot.present;
1556        let count = slot.count;
1557
1558        if !have_meta {
1559            // Until fragment zero arrives we cannot know how to present the
1560            // message (it may be an edit); keep accumulating silently.
1561            return;
1562        }
1563
1564        if complete && is_edit {
1565            let mut original_id = self.pool.slots[slot_index]
1566                .as_ref()
1567                .expect("occupied")
1568                .meta
1569                .editing
1570                .expect("checked");
1571            if let Some(stream) = self.inbound.get_mut(&key) {
1572                original_id = stream.edit_refs.resolve(original_id);
1573                stream.edit_refs.record(id, original_id);
1574            }
1575            // As in the unfragmented path: the superseded original must not
1576            // render beside the edit if a copy of it straggles in late.
1577            if let Some(stream) = self.inbound.get_mut(&key) {
1578                stream.seen.insert(original_id);
1579            }
1580            let original = self
1581                .inbound
1582                .get(&key)
1583                .and_then(|stream| stream.refs.lookup(original_id))
1584                .map(ResolvedRef::Handle)
1585                .unwrap_or(ResolvedRef::Unresolved(
1586                    crate::model::WireRef::SenderScoped {
1587                        sender: key.sender,
1588                        message_id: original_id,
1589                    },
1590                ));
1591            let body_ref = self.render_to_arena(slot_index, true);
1592            // A reassembly that reused a gap placeholder turned out to be an
1593            // edit, not a standalone bubble: retire the spinner row and apply
1594            // the edit under a fresh handle.
1595            if late {
1596                self.emit_mutation(
1597                    handle,
1598                    MutationKind::Delete {
1599                        conversation: key.conversation,
1600                        original: ResolvedRef::Handle(handle),
1601                    },
1602                );
1603                if let Some(stream) = self.inbound.get_mut(&key) {
1604                    stream.refs.retire(id);
1605                }
1606            }
1607            // An edit whose *target* is a still-missing gap fills that slot
1608            // with the edited content instead of dangling until the repair of
1609            // superseded content exhausts.
1610            {
1611                let mut scratch = [0u8; REASSEMBLED_BODY_MAX + 64];
1612                let len = (body_ref.len as usize).min(scratch.len());
1613                let start = body_ref.offset as usize;
1614                scratch[..len].copy_from_slice(&self.arena[start..start + len]);
1615                if self.fill_gap_with_edit(envelope, key, original_id, &scratch[..len]) {
1616                    self.pool.close_slot(slot_index);
1617                    return;
1618                }
1619            }
1620            let edit_handle = if late { self.alloc_handle() } else { handle };
1621            let kind = if body_ref.len == 0 {
1622                MutationKind::Delete {
1623                    conversation: key.conversation,
1624                    original,
1625                }
1626            } else {
1627                MutationKind::Edit {
1628                    conversation: key.conversation,
1629                    original,
1630                    body: body_ref,
1631                }
1632            };
1633            self.emit_mutation(edit_handle, kind);
1634            self.pool.close_slot(slot_index);
1635            return;
1636        }
1637        if is_edit {
1638            // Fragmented edit still incomplete: not displayed until whole.
1639            return;
1640        }
1641
1642        let status = if complete {
1643            CompletionStatus::Complete
1644        } else {
1645            CompletionStatus::Partial {
1646                present,
1647                count,
1648                finalized: false,
1649            }
1650        };
1651        let body_ref = self.render_to_arena(slot_index, complete);
1652        // Notify exactly once, when the message becomes complete (the notify
1653        // deadline in `tick` covers messages that stall incomplete).
1654        let notify = complete && !notified;
1655        if notify && let Some(slot) = self.pool.slots[slot_index].as_mut() {
1656            slot.notified = true;
1657        }
1658
1659        if !announced {
1660            let slot = self.pool.slots[slot_index].as_mut().expect("occupied");
1661            slot.announced = true;
1662            let meta = slot.meta;
1663            let message_type = meta.message_type();
1664            let sender_handle = content
1665                .sender_handle
1666                .and_then(|text| self.arena_store(text));
1667            let regarding = meta
1668                .regarding
1669                .map(|r| self.resolved_from_wire(key.conversation, Some(key.sender), r));
1670            self.emit_mutation(
1671                handle,
1672                MutationKind::Insert {
1673                    conversation: envelope.conversation,
1674                    sender: envelope.sender,
1675                    direction: Direction::Inbound,
1676                    message_type,
1677                    wire_id: Some(id),
1678                    epoch: self.inbound.get(&key).map(|s| s.epoch).unwrap_or(0),
1679                    client_token: None,
1680                    sender_handle,
1681                    regarding,
1682                    bg_color: content.bg_color,
1683                    text_color: content.text_color,
1684                    body: body_ref,
1685                    status,
1686                    presence: Presence::Present,
1687                    late,
1688                    notify,
1689                },
1690            );
1691            if let Some(stream) = self.inbound.get_mut(&key) {
1692                stream.refs.record(id, handle);
1693            }
1694        } else {
1695            self.emit_mutation(
1696                handle,
1697                MutationKind::UpdateBody {
1698                    body: body_ref,
1699                    status,
1700                    late: false,
1701                    notify,
1702                },
1703            );
1704        }
1705
1706        if complete {
1707            self.pool.close_slot(slot_index);
1708            let was_repairing = self.inbound.get_mut(&key).is_some_and(|stream| {
1709                let repairing = stream
1710                    .pending
1711                    .iter()
1712                    .any(|pending| pending.message_id == id && pending.attempts > 0);
1713                stream.cancel_pending(id, None);
1714                repairing
1715            });
1716            if was_repairing {
1717                self.push_output(Output::Event(Event::RepairFinished {
1718                    conversation: key.conversation,
1719                    sender: key.sender,
1720                    message_id: id,
1721                    outcome: RepairOutcome::Repaired,
1722                }));
1723            }
1724        }
1725        let _ = now_ms;
1726    }
1727
1728    #[allow(clippy::too_many_arguments)]
1729    fn insert_content(
1730        &mut self,
1731        envelope: &Envelope,
1732        content: &validate::ContentMessage<'_>,
1733        wire_id: Option<u8>,
1734        status: CompletionStatus,
1735        late: bool,
1736        notify: bool,
1737        reuse: Option<MessageHandle>,
1738        now_ms: u64,
1739    ) -> MessageHandle {
1740        let handle = reuse.unwrap_or_else(|| self.alloc_handle());
1741        let body = core::str::from_utf8(content.body).unwrap_or("");
1742        let body_ref = self
1743            .arena_store(body)
1744            .unwrap_or(BodyRef { offset: 0, len: 0 });
1745        let sender_handle = content
1746            .sender_handle
1747            .and_then(|handle_text| self.arena_store(handle_text));
1748        let regarding = content
1749            .regarding
1750            .map(|r| self.resolved_from_wire(envelope.conversation, Some(envelope.sender), r));
1751        let epoch = self
1752            .inbound
1753            .get(&StreamKey {
1754                conversation: envelope.conversation,
1755                sender: envelope.sender,
1756            })
1757            .map(|stream| stream.epoch)
1758            .unwrap_or(0);
1759        self.emit_mutation(
1760            handle,
1761            MutationKind::Insert {
1762                conversation: envelope.conversation,
1763                sender: envelope.sender,
1764                direction: Direction::Inbound,
1765                message_type: content.message_type,
1766                wire_id,
1767                epoch,
1768                client_token: None,
1769                sender_handle,
1770                regarding,
1771                bg_color: content.bg_color,
1772                text_color: content.text_color,
1773                body: body_ref,
1774                status,
1775                presence: Presence::Present,
1776                late,
1777                notify,
1778            },
1779        );
1780        let _ = now_ms;
1781        handle
1782    }
1783
1784    // ------------------------------------------------------------------
1785    // Resend service
1786    // ------------------------------------------------------------------
1787
1788    fn receive_resend_request(
1789        &mut self,
1790        envelope: &Envelope,
1791        sequence: MessageSequence,
1792        channel_group: bool,
1793        now_ms: u64,
1794    ) {
1795        // The requester must be an individually attributable peer.
1796        let SenderScope::Peer(requester) = envelope.sender else {
1797            self.push_output(Output::Diagnostic(Diagnostic::UnattributableResend));
1798            return;
1799        };
1800        // Select the archive stream from the arrival path and flag.
1801        let conversation = if channel_group {
1802            match envelope.conversation {
1803                ConversationKey::ChannelDirect { channel, .. } => {
1804                    ConversationKey::ChannelGroup { channel }
1805                }
1806                _ => {
1807                    self.push_output(Output::Diagnostic(Diagnostic::UnattributableResend));
1808                    return;
1809                }
1810            }
1811        } else {
1812            envelope.conversation
1813        };
1814
1815        if self.coalesce.recently_answered(
1816            &conversation,
1817            &sequence,
1818            now_ms,
1819            self.config.coalesce_window_ms,
1820        ) {
1821            self.push_output(Output::Diagnostic(Diagnostic::CoalescedResend {
1822                message_id: sequence.message_id,
1823            }));
1824            return;
1825        }
1826
1827        // The requested frame is still in flight on this node's own radio —
1828        // queued behind earlier frames or awaiting its delivery report. On a
1829        // slow serialized link the requester's patience can lapse before the
1830        // original arrives; answering now would duplicate the frame on air
1831        // and delay everything queued behind it. A genuinely lost frame
1832        // leaves `in_flight` with its failure report, after which requests
1833        // are served normally. Only meaningful on platforms that report
1834        // transport progress; without reports, emission tells us nothing
1835        // about whether the frame is still queued.
1836        let requested_fragment = sequence.fragment.map(|fragment| fragment.index);
1837        if self.saw_transmit_report
1838            && self.in_flight.iter().any(|frame| {
1839                frame.archive.is_some_and(|archive| {
1840                    archive.conversation == conversation
1841                        && archive.message_id == sequence.message_id
1842                        && archive.fragment == requested_fragment
1843                })
1844            })
1845        {
1846            self.push_output(Output::Diagnostic(Diagnostic::CoalescedResend {
1847                message_id: sequence.message_id,
1848            }));
1849            return;
1850        }
1851
1852        // A lookup for the same frame is already outstanding: one response
1853        // will serve both requesters.
1854        if self
1855            .lookups
1856            .iter()
1857            .any(|lookup| lookup.conversation == conversation && lookup.sequence == sequence)
1858        {
1859            self.push_output(Output::Diagnostic(Diagnostic::CoalescedResend {
1860                message_id: sequence.message_id,
1861            }));
1862            return;
1863        }
1864
1865        let request_id = self.next_request;
1866        self.next_request = self.next_request.wrapping_add(1);
1867        if self.lookups.is_full() {
1868            self.lookups.remove(0);
1869        }
1870        let _ = self.lookups.push(PendingLookup {
1871            request_id,
1872            conversation,
1873            requester,
1874            sequence,
1875        });
1876        self.push_output(Output::LookupOutbound {
1877            request_id,
1878            conversation,
1879            sequence,
1880        });
1881    }
1882
1883    fn receive_unavailable(&mut self, envelope: &Envelope, sequence: MessageSequence, now_ms: u64) {
1884        let key = StreamKey {
1885            conversation: envelope.conversation,
1886            sender: envelope.sender,
1887        };
1888        let id = sequence.message_id;
1889        let fragment = sequence.fragment.map(|fragment| fragment.index);
1890
1891        // A whole-message gap placeholder (no reassembly slot) reserved for
1892        // this ID becomes a permanent loss marker below.
1893        let placeholder = self.inbound.get(&key).and_then(|stream| {
1894            stream
1895                .pending
1896                .iter()
1897                .find(|pending| pending.message_id == id && pending.fragment.is_none())
1898                .and_then(|pending| pending.handle)
1899        });
1900
1901        if let Some(stream) = self.inbound.get_mut(&key) {
1902            stream.cancel_pending(id, fragment);
1903            // The position is accounted for; it no longer counts as a gap.
1904            stream.seen.insert(id);
1905            stream.last_active_ms = now_ms;
1906        }
1907
1908        let epoch = self
1909            .inbound
1910            .get(&key)
1911            .map(|stream| stream.epoch)
1912            .unwrap_or(0);
1913        let slot = self.pool.find_slot(&key, epoch, id);
1914        if fragment.is_none()
1915            && slot.is_none()
1916            && let Some(handle) = placeholder
1917        {
1918            self.flip_placeholder_unavailable(key, id, handle);
1919        }
1920        if let Some(slot_index) = slot {
1921            match fragment {
1922                Some(index) => {
1923                    let slot = self.pool.slots[slot_index].as_mut().expect("occupied");
1924                    let bit = 1u16 << index;
1925                    if slot.present & bit == 0 {
1926                        slot.unavailable |= bit;
1927                    }
1928                    let settled = {
1929                        let slot = self.pool.slots[slot_index].as_ref().expect("occupied");
1930                        slot.is_settled()
1931                    };
1932                    if settled {
1933                        self.finalize_slot(slot_index, now_ms, RepairOutcome::Unavailable, false);
1934                    } else if self.pool.slots[slot_index]
1935                        .as_ref()
1936                        .is_some_and(|slot| slot.announced)
1937                    {
1938                        let body = self.render_to_arena(slot_index, false);
1939                        let slot = self.pool.slots[slot_index].as_ref().expect("occupied");
1940                        let status = CompletionStatus::Partial {
1941                            present: slot.present,
1942                            count: slot.count,
1943                            finalized: false,
1944                        };
1945                        let handle = slot.handle;
1946                        self.emit_mutation(
1947                            handle,
1948                            MutationKind::UpdateBody {
1949                                body,
1950                                status,
1951                                late: false,
1952                                notify: false,
1953                            },
1954                        );
1955                    }
1956                }
1957                None => {
1958                    self.finalize_slot(slot_index, now_ms, RepairOutcome::Unavailable, false);
1959                }
1960            }
1961        }
1962
1963        self.push_output(Output::Event(Event::MessageUnavailable {
1964            conversation: envelope.conversation,
1965            sender: envelope.sender,
1966            message_id: id,
1967            fragment,
1968        }));
1969    }
1970
1971    // ------------------------------------------------------------------
1972    // Timers
1973    // ------------------------------------------------------------------
1974
1975    fn expire_slots(&mut self, now_ms: u64) {
1976        for index in 0..SLOTS {
1977            let expired = self.pool.slots[index]
1978                .as_ref()
1979                .is_some_and(|slot| now_ms >= slot.deadline_ms);
1980            if expired {
1981                self.finalize_slot(index, now_ms, RepairOutcome::Expired, true);
1982            }
1983        }
1984    }
1985
1986    /// Queue repair entries for missing fragments of stalled assemblies.
1987    fn schedule_fragment_repairs(&mut self, now_ms: u64) {
1988        for index in 0..SLOTS {
1989            let Some(slot) = self.pool.slots[index].as_ref() else {
1990                continue;
1991            };
1992            if now_ms < slot.repair_at_ms || slot.is_settled() {
1993                continue;
1994            }
1995            let key = slot.stream;
1996            let id = slot.message_id;
1997            if let Some(stream) = self.inbound.get_mut(&key) {
1998                if stream.collided {
1999                    continue;
2000                }
2001                // A fragmented message advances through missing fragments
2002                // serially. Do not queue an entire missing bitmap at once:
2003                // one request receives its full retry budget before repair
2004                // moves to the next fragment.
2005                if stream
2006                    .pending
2007                    .iter()
2008                    .any(|pending| pending.message_id == id)
2009                {
2010                    continue;
2011                }
2012                let next = self.pool.slots[index]
2013                    .as_ref()
2014                    .and_then(|slot| slot.repairable_missing().next());
2015                if let Some(fragment) = next {
2016                    let _ = stream.pending.push(PendingRepair {
2017                        message_id: id,
2018                        fragment: Some(fragment),
2019                        deadline_ms: now_ms,
2020                        attempts: 0,
2021                        handle: None,
2022                    });
2023                    if let Some(slot) = self.pool.slots[index].as_mut() {
2024                        slot.repair_at_ms = now_ms + self.config.request_retry_ms;
2025                    }
2026                }
2027            }
2028        }
2029    }
2030
2031    fn transmit_due_repairs(&mut self, now_ms: u64) {
2032        let mut budget = self.config.max_requests_per_tick;
2033        let keys: heapless::Vec<StreamKey, 16> = self.inbound.keys().copied().collect();
2034        for key in keys {
2035            if budget == 0 {
2036                break;
2037            }
2038            let Some(stream) = self.inbound.get(&key) else {
2039                continue;
2040            };
2041            if stream.collided
2042                || now_ms.saturating_sub(stream.last_request_ms)
2043                    < self.config.min_request_interval_ms
2044            {
2045                continue;
2046            }
2047            let Some(position) = stream
2048                .pending
2049                .iter()
2050                .position(|pending| now_ms >= pending.deadline_ms)
2051            else {
2052                continue;
2053            };
2054            let pending = stream.pending[position];
2055
2056            // Resolve the request destination.
2057            let destination = match key.conversation {
2058                ConversationKey::Direct { peer } => Destination::Peer(peer),
2059                ConversationKey::Room { room } => Destination::Peer(room),
2060                ConversationKey::ChannelDirect { channel, peer } => {
2061                    Destination::ChannelPeer { channel, peer }
2062                }
2063                ConversationKey::ChannelGroup { channel } => match stream.sender_key {
2064                    Some(peer) => Destination::ChannelPeer { channel, peer },
2065                    None => {
2066                        // Unaddressable: give up on this frame; expiry will
2067                        // finalize any partial render.
2068                        let stream = self.inbound.get_mut(&key).expect("present");
2069                        stream.pending.remove(position);
2070                        self.push_output(Output::Event(Event::RepairFinished {
2071                            conversation: key.conversation,
2072                            sender: key.sender,
2073                            message_id: pending.message_id,
2074                            outcome: RepairOutcome::Unaddressable,
2075                        }));
2076                        if pending.fragment.is_none()
2077                            && let Some(handle) = pending.handle
2078                        {
2079                            self.flip_placeholder_unavailable(key, pending.message_id, handle);
2080                        }
2081                        continue;
2082                    }
2083                },
2084            };
2085            let channel_group = matches!(key.conversation, ConversationKey::ChannelGroup { .. });
2086
2087            // A fragment request needs the slot's fragment count.
2088            let sequence = match pending.fragment {
2089                None => MessageSequence::unfragmented(pending.message_id),
2090                Some(index) => {
2091                    let epoch = stream.epoch;
2092                    let count = self
2093                        .pool
2094                        .find_slot(&key, epoch, pending.message_id)
2095                        .and_then(|slot| self.pool.slots[slot].as_ref())
2096                        .map(|slot| slot.count);
2097                    let Some(count) = count else {
2098                        let stream = self.inbound.get_mut(&key).expect("present");
2099                        stream.pending.remove(position);
2100                        continue;
2101                    };
2102                    MessageSequence {
2103                        message_id: pending.message_id,
2104                        fragment: Some(Fragment { index, count }),
2105                    }
2106                }
2107            };
2108
2109            let request = TextMessage {
2110                message_type: MessageType::ResendRequest,
2111                sequence: Some(sequence),
2112                channel_group_resend: channel_group,
2113                ..TextMessage::basic("")
2114            };
2115            let mut buffer = [0u8; MAX_FRAME];
2116            let Ok(len) = codec::encode(&request, &mut buffer) else {
2117                continue;
2118            };
2119            let mut frame = heapless::Vec::new();
2120            let _ = frame.extend_from_slice(&buffer[..len]);
2121            self.queue_transmit(destination, None, frame, None);
2122            budget -= 1;
2123
2124            if pending.attempts == 0 {
2125                self.push_output(Output::Event(Event::RepairStarted {
2126                    conversation: key.conversation,
2127                    sender: key.sender,
2128                    message_id: pending.message_id,
2129                    fragment: pending.fragment,
2130                }));
2131            }
2132
2133            let max_attempts = self.config.max_repair_attempts;
2134            let retry_ms = self.config.request_retry_ms;
2135            let stream = self.inbound.get_mut(&key).expect("present");
2136            stream.last_request_ms = now_ms;
2137            let entry = &mut stream.pending[position];
2138            entry.attempts += 1;
2139            if entry.attempts >= max_attempts {
2140                let message_id = entry.message_id;
2141                let fragment = entry.fragment;
2142                let placeholder = entry.handle;
2143                stream.pending.remove(position);
2144                if let Some(fragment) = fragment
2145                    && let Some(slot_index) = self.pool.find_slot(&key, stream.epoch, message_id)
2146                    && let Some(slot) = self.pool.slots[slot_index].as_mut()
2147                {
2148                    slot.repair_exhausted |= 1u16 << fragment;
2149                }
2150                self.push_output(Output::Event(Event::RepairFinished {
2151                    conversation: key.conversation,
2152                    sender: key.sender,
2153                    message_id,
2154                    outcome: RepairOutcome::Exhausted,
2155                }));
2156                // A whole-message gap that exhausted its repair budget: turn
2157                // its reserved slot into a permanent loss marker.
2158                if fragment.is_none()
2159                    && let Some(handle) = placeholder
2160                {
2161                    self.flip_placeholder_unavailable(key, message_id, handle);
2162                }
2163            } else {
2164                entry.deadline_ms = now_ms + retry_ms;
2165            }
2166        }
2167    }
2168
2169    /// Finalize a slot: emit its final partial render (when displayable) and
2170    /// release its pages.
2171    fn finalize_slot(
2172        &mut self,
2173        slot_index: usize,
2174        now_ms: u64,
2175        outcome: RepairOutcome,
2176        evicted: bool,
2177    ) {
2178        let Some(slot) = self.pool.slots[slot_index].as_ref() else {
2179            return;
2180        };
2181        let key = slot.stream;
2182        let id = slot.message_id;
2183        let announced = slot.announced;
2184        let handle = slot.handle;
2185        let complete = slot.is_complete();
2186
2187        if announced && !complete {
2188            let body = self.render_to_arena(slot_index, true);
2189            let slot = self.pool.slots[slot_index].as_ref().expect("occupied");
2190            let status = CompletionStatus::Partial {
2191                present: slot.present,
2192                count: slot.count,
2193                finalized: true,
2194            };
2195            self.emit_mutation(
2196                handle,
2197                MutationKind::UpdateBody {
2198                    body,
2199                    status,
2200                    late: false,
2201                    notify: false,
2202                },
2203            );
2204        }
2205        self.pool.close_slot(slot_index);
2206        if evicted {
2207            self.push_output(Output::Diagnostic(Diagnostic::ReassemblyEvicted {
2208                message_id: id,
2209            }));
2210        }
2211        if let Some(stream) = self.inbound.get_mut(&key) {
2212            stream.cancel_pending(id, None);
2213            stream.last_active_ms = now_ms;
2214        }
2215        self.push_output(Output::Event(Event::RepairFinished {
2216            conversation: key.conversation,
2217            sender: key.sender,
2218            message_id: id,
2219            outcome,
2220        }));
2221    }
2222
2223    // ------------------------------------------------------------------
2224    // Outbound encoding
2225    // ------------------------------------------------------------------
2226
2227    fn encode_and_queue(
2228        &mut self,
2229        conversation: ConversationKey,
2230        handle: MessageHandle,
2231        template: &TextMessage<'_>,
2232        body: &[u8],
2233        message_id: u8,
2234    ) -> Result<(), ComposeError> {
2235        let destination = destination_for(&conversation);
2236
2237        // Trial-encode with an empty body to learn the option overhead.
2238        let mut trial = *template;
2239        trial.body = &[];
2240        let mut buffer = [0u8; MAX_FRAME];
2241        let overhead = codec::encode(&trial, &mut buffer).map_err(ComposeError::Encode)?;
2242        let single_budget = MAX_FRAME.saturating_sub(overhead + 1);
2243
2244        let Some(plan) =
2245            FragmentPlan::plan(body.len(), single_budget).map_err(|_| ComposeError::TooLarge)?
2246        else {
2247            let mut message = *template;
2248            message.body = body;
2249            let len = codec::encode(&message, &mut buffer).map_err(ComposeError::Encode)?;
2250            let mut frame = heapless::Vec::new();
2251            let _ = frame.extend_from_slice(&buffer[..len]);
2252            self.queue_transmit(
2253                destination,
2254                Some(ArchiveKey {
2255                    conversation,
2256                    message_id,
2257                    fragment: None,
2258                }),
2259                frame,
2260                Some((handle, None)),
2261            );
2262            return Ok(());
2263        };
2264
2265        for index in 0..plan.count {
2266            let range = plan.range(index);
2267            let mut message = if index == 0 {
2268                *template
2269            } else {
2270                // Continuation fragments carry only sequence metadata.
2271                let mut continuation = TextMessage::basic("");
2272                continuation.sequence = template.sequence;
2273                continuation
2274            };
2275            message.sequence = Some(MessageSequence {
2276                message_id,
2277                fragment: Some(Fragment {
2278                    index,
2279                    count: plan.count,
2280                }),
2281            });
2282            message.body = &body[range];
2283            let len = codec::encode(&message, &mut buffer).map_err(ComposeError::Encode)?;
2284            if len > MAX_FRAME {
2285                return Err(ComposeError::TooLarge);
2286            }
2287            let mut frame = heapless::Vec::new();
2288            let _ = frame.extend_from_slice(&buffer[..len]);
2289            self.queue_transmit(
2290                destination,
2291                Some(ArchiveKey {
2292                    conversation,
2293                    message_id,
2294                    fragment: Some(index),
2295                }),
2296                frame,
2297                Some((handle, Some(index))),
2298            );
2299        }
2300        Ok(())
2301    }
2302
2303    /// Re-encode `body` under the original message ID and emit it as
2304    /// archive-only material (never transmitted): the resend service will
2305    /// serve this in place of the superseded original. The replacement is a
2306    /// plain content frame — the option set the original carried is not
2307    /// retained by the engine, and a requester that missed the original only
2308    /// needs its current content at its sequence position. Best-effort: an
2309    /// encode failure leaves the ID's archive empty (the preceding
2310    /// [`Output::DeleteArchive`] already retired the original), which the
2311    /// resend service answers as Message Unavailable — never stale content.
2312    fn archive_replacement(&mut self, conversation: ConversationKey, message_id: u8, body: &[u8]) {
2313        let mut template = TextMessage::basic("");
2314        template.sequence = Some(MessageSequence::unfragmented(message_id));
2315
2316        let mut buffer = [0u8; MAX_FRAME];
2317        let Ok(overhead) = codec::encode(&template, &mut buffer) else {
2318            return;
2319        };
2320        let single_budget = MAX_FRAME.saturating_sub(overhead + 1);
2321        let Ok(plan) = FragmentPlan::plan(body.len(), single_budget) else {
2322            return;
2323        };
2324
2325        let Some(plan) = plan else {
2326            let mut message = template;
2327            message.body = body;
2328            let Ok(len) = codec::encode(&message, &mut buffer) else {
2329                return;
2330            };
2331            let mut frame = heapless::Vec::new();
2332            let _ = frame.extend_from_slice(&buffer[..len]);
2333            self.push_output(Output::StoreArchive {
2334                key: ArchiveKey {
2335                    conversation,
2336                    message_id,
2337                    fragment: None,
2338                },
2339                payload: frame,
2340            });
2341            return;
2342        };
2343
2344        for index in 0..plan.count {
2345            let range = plan.range(index);
2346            let mut message = TextMessage::basic("");
2347            message.sequence = Some(MessageSequence {
2348                message_id,
2349                fragment: Some(Fragment {
2350                    index,
2351                    count: plan.count,
2352                }),
2353            });
2354            message.body = &body[range];
2355            let Ok(len) = codec::encode(&message, &mut buffer) else {
2356                return;
2357            };
2358            let mut frame = heapless::Vec::new();
2359            let _ = frame.extend_from_slice(&buffer[..len]);
2360            self.push_output(Output::StoreArchive {
2361                key: ArchiveKey {
2362                    conversation,
2363                    message_id,
2364                    fragment: Some(index),
2365                },
2366                payload: frame,
2367            });
2368        }
2369    }
2370
2371    fn queue_transmit(
2372        &mut self,
2373        destination: Destination,
2374        archive: Option<ArchiveKey>,
2375        payload: heapless::Vec<u8, MAX_FRAME>,
2376        track: Option<(MessageHandle, Option<u8>)>,
2377    ) -> u32 {
2378        let transmission_id = self.next_transmission;
2379        self.next_transmission = self.next_transmission.wrapping_add(1);
2380        if let Some((handle, fragment)) = track {
2381            if self.in_flight.is_full() {
2382                self.in_flight.remove(0);
2383            }
2384            let _ = self.in_flight.push(InFlightFrame {
2385                transmission_id,
2386                handle,
2387                fragment,
2388                archive,
2389                expects_ack: matches!(destination, Destination::Peer(_)),
2390            });
2391        }
2392        self.push_output(Output::Transmit(Transmission {
2393            transmission_id,
2394            destination,
2395            archive,
2396            payload,
2397        }));
2398        transmission_id
2399    }
2400
2401    // ------------------------------------------------------------------
2402    // Reference resolution
2403    // ------------------------------------------------------------------
2404
2405    /// Build the wire Regarding form for a locally known message handle.
2406    fn wire_reference_for(
2407        &self,
2408        conversation: ConversationKey,
2409        handle: MessageHandle,
2410    ) -> Option<Regarding> {
2411        let multicast = conversation.uses_multicast_references();
2412        if let Some(stream) = self.outbound.get(&conversation)
2413            && let Some(id) = stream.refs.lookup_handle(handle)
2414        {
2415            return Some(if multicast {
2416                Regarding::Multicast {
2417                    message_id: id,
2418                    source_prefix: umsh_core::NodeHint([
2419                        self.local_key.0[0],
2420                        self.local_key.0[1],
2421                        self.local_key.0[2],
2422                    ]),
2423                }
2424            } else {
2425                Regarding::Unicast { message_id: id }
2426            });
2427        }
2428        for (key, stream) in self.inbound.iter() {
2429            if key.conversation != conversation {
2430                continue;
2431            }
2432            if let Some(id) = stream.refs.lookup_handle(handle) {
2433                return Some(if multicast {
2434                    let prefix = key.sender.hint()?;
2435                    Regarding::Multicast {
2436                        message_id: id,
2437                        source_prefix: prefix,
2438                    }
2439                } else {
2440                    Regarding::Unicast { message_id: id }
2441                });
2442            }
2443        }
2444        None
2445    }
2446
2447    /// Resolve a compose-time regarding target into the wire form to send and
2448    /// the reference to export on the transcript mutation.
2449    fn resolve_regarding(
2450        &self,
2451        conversation: ConversationKey,
2452        target: RegardingRef,
2453    ) -> Result<(Regarding, ResolvedRef), ComposeError> {
2454        let (message_id, direction, sender_hint, epoch) = match target {
2455            RegardingRef::Handle(handle) => {
2456                let wire = self
2457                    .wire_reference_for(conversation, handle)
2458                    .ok_or(ComposeError::UnknownRegarding)?;
2459                return Ok((wire, ResolvedRef::Handle(handle)));
2460            }
2461            RegardingRef::Wire {
2462                message_id,
2463                direction,
2464                sender_hint,
2465                epoch,
2466            } => (message_id, direction, sender_hint, epoch),
2467        };
2468
2469        let sender = regarding_sender(conversation, direction, sender_hint)
2470            .ok_or(ComposeError::UnknownRegarding)?;
2471        match direction {
2472            Direction::Outbound => {
2473                let stream = self
2474                    .outbound
2475                    .get(&conversation)
2476                    .ok_or(ComposeError::UnknownRegarding)?;
2477                // A reference that predates a Sequence Reset names an ID the
2478                // receiver has already discarded as a target.
2479                if stream.announce_reset || epoch.is_some_and(|epoch| stream.epoch != epoch) {
2480                    return Err(ComposeError::UnknownRegarding);
2481                }
2482                // The ID must lie in the already allocated serial half-space.
2483                let delta = stream.next_id.wrapping_sub(message_id);
2484                if delta == 0 || delta > 128 {
2485                    return Err(ComposeError::UnknownRegarding);
2486                }
2487            }
2488            Direction::Inbound => {
2489                let live = self.inbound.get(&StreamKey {
2490                    conversation,
2491                    sender,
2492                });
2493                if let Some(stream) = live
2494                    && epoch.is_some_and(|epoch| stream.epoch != epoch)
2495                {
2496                    return Err(ComposeError::UnknownRegarding);
2497                }
2498            }
2499        }
2500
2501        let wire = if conversation.uses_multicast_references() {
2502            let source_prefix = match direction {
2503                Direction::Outbound => umsh_core::NodeHint([
2504                    self.local_key.0[0],
2505                    self.local_key.0[1],
2506                    self.local_key.0[2],
2507                ]),
2508                Direction::Inbound => sender.hint().ok_or(ComposeError::UnknownRegarding)?,
2509            };
2510            Regarding::Multicast {
2511                message_id,
2512                source_prefix,
2513            }
2514        } else {
2515            Regarding::Unicast { message_id }
2516        };
2517        // No live handle backs a wire-addressed target; the platform matches
2518        // the exported reference against its own persisted rows.
2519        Ok((
2520            wire,
2521            ResolvedRef::Unresolved(crate::model::WireRef::SenderScoped { sender, message_id }),
2522        ))
2523    }
2524
2525    /// Resolve a received wire reference to a stable handle when unambiguous.
2526    fn resolved_from_wire(
2527        &self,
2528        conversation: ConversationKey,
2529        sender: Option<SenderScope>,
2530        regarding: Regarding,
2531    ) -> ResolvedRef {
2532        match regarding {
2533            Regarding::Multicast {
2534                message_id,
2535                source_prefix,
2536            } => {
2537                let local_prefix = umsh_core::NodeHint([
2538                    self.local_key.0[0],
2539                    self.local_key.0[1],
2540                    self.local_key.0[2],
2541                ]);
2542                if source_prefix == local_prefix
2543                    && let Some(stream) = self.outbound.get(&conversation)
2544                {
2545                    let message_id = stream.edit_refs.resolve(message_id);
2546                    if let Some(handle) = stream.refs.lookup(message_id) {
2547                        return ResolvedRef::Handle(handle);
2548                    }
2549                }
2550                let key = StreamKey {
2551                    conversation,
2552                    sender: SenderScope::ClaimedMember(source_prefix),
2553                };
2554                // A reference naming an edit stands for the message it
2555                // replaced; carry the collapsed ID into the fallback too, so
2556                // the platform matches the row it actually stored.
2557                let message_id = self
2558                    .inbound
2559                    .get(&key)
2560                    .map(|stream| stream.edit_refs.resolve(message_id))
2561                    .unwrap_or(message_id);
2562                let unresolved = ResolvedRef::Unresolved(crate::model::WireRef::SenderScoped {
2563                    sender: SenderScope::ClaimedMember(source_prefix),
2564                    message_id,
2565                });
2566                match self.inbound.get(&key) {
2567                    Some(stream) if !stream.collided => stream
2568                        .refs
2569                        .lookup(message_id)
2570                        .map(ResolvedRef::Handle)
2571                        .unwrap_or(unresolved),
2572                    _ => unresolved,
2573                }
2574            }
2575            Regarding::Unicast { message_id } => {
2576                // In a one-to-one conversation the reference may target
2577                // either party's stream; resolve only when unambiguous.
2578                let inbound_key = sender.map(|sender| StreamKey {
2579                    conversation,
2580                    sender,
2581                });
2582                let inbound_stream = inbound_key.and_then(|key| self.inbound.get(&key));
2583                let outbound_stream = self.outbound.get(&conversation);
2584                // Either side may have edited the target; collapse through
2585                // whichever stream knows the ID as an edit of its own.
2586                let message_id = inbound_stream
2587                    .map(|stream| stream.edit_refs.resolve(message_id))
2588                    .filter(|resolved| *resolved != message_id)
2589                    .or_else(|| outbound_stream.map(|stream| stream.edit_refs.resolve(message_id)))
2590                    .unwrap_or(message_id);
2591                let inbound = inbound_stream.and_then(|stream| stream.refs.lookup(message_id));
2592                let outbound = outbound_stream.and_then(|stream| stream.refs.lookup(message_id));
2593                match (inbound, outbound) {
2594                    (Some(handle), None) | (None, Some(handle)) => ResolvedRef::Handle(handle),
2595                    _ => ResolvedRef::Unresolved(match sender {
2596                        Some(sender) => crate::model::WireRef::SenderScoped { sender, message_id },
2597                        None => crate::model::WireRef::RoomCanonical { message_id },
2598                    }),
2599                }
2600            }
2601        }
2602    }
2603
2604    // ------------------------------------------------------------------
2605    // Infrastructure
2606    // ------------------------------------------------------------------
2607
2608    fn ensure_outbound(&mut self, conversation: ConversationKey, now_ms: u64) {
2609        if self.outbound.contains_key(&conversation) {
2610            return;
2611        }
2612        if self.outbound.len() == self.outbound.capacity()
2613            && let Some(oldest) = self
2614                .outbound
2615                .iter()
2616                .min_by_key(|(_, stream)| stream.last_active_ms)
2617                .map(|(key, _)| *key)
2618        {
2619            // Demote the evicted stream's continuity to the cold stash so
2620            // reactivation resumes its sequence instead of resetting.
2621            if let Some(stream) = self.outbound.remove(&oldest)
2622                && !stream.announce_reset
2623            {
2624                self.stash_checkpoint(oldest, stream.next_id, stream.epoch);
2625            }
2626            self.push_output(Output::Diagnostic(Diagnostic::StreamEvicted));
2627        }
2628        let cold = self
2629            .cold_checkpoints
2630            .iter()
2631            .position(|(key, _, _)| *key == conversation);
2632        let stream = match cold {
2633            Some(position) => {
2634                let (_, next_id, epoch) = self.cold_checkpoints.remove(position);
2635                OutboundStream {
2636                    next_id,
2637                    epoch,
2638                    announce_reset: false,
2639                    refs: Default::default(),
2640                    edit_refs: Default::default(),
2641                    last_active_ms: now_ms,
2642                }
2643            }
2644            None => OutboundStream::fresh(now_ms),
2645        };
2646        let _ = self.outbound.insert(conversation, stream);
2647    }
2648
2649    /// Record `(next_id, epoch)` continuity for an inactive conversation,
2650    /// displacing the oldest entry when the stash is full.
2651    fn stash_checkpoint(&mut self, conversation: ConversationKey, next_id: u8, epoch: u16) {
2652        self.cold_checkpoints
2653            .retain(|(key, _, _)| *key != conversation);
2654        if self.cold_checkpoints.is_full() {
2655            self.cold_checkpoints.remove(0);
2656        }
2657        let _ = self.cold_checkpoints.push((conversation, next_id, epoch));
2658    }
2659
2660    fn ensure_inbound(&mut self, key: StreamKey, now_ms: u64) {
2661        if self.inbound.contains_key(&key) {
2662            return;
2663        }
2664        if self.inbound.len() == self.inbound.capacity()
2665            && let Some(oldest) = self
2666                .inbound
2667                .iter()
2668                .min_by_key(|(_, stream)| stream.last_active_ms)
2669                .map(|(key, _)| *key)
2670        {
2671            self.pool.drop_stream(&oldest);
2672            self.inbound.remove(&oldest);
2673            self.push_output(Output::Diagnostic(Diagnostic::StreamEvicted));
2674        }
2675        let _ = self.inbound.insert(key, InboundStream::new(now_ms));
2676    }
2677
2678    fn alloc_handle(&mut self) -> MessageHandle {
2679        let handle = MessageHandle(self.next_handle);
2680        self.next_handle = self.next_handle.wrapping_add(1);
2681        handle
2682    }
2683
2684    /// Reserve an ordered transcript slot for a detected sequence gap: emit a
2685    /// `GapPending` placeholder (empty body, spinner on the host) and register
2686    /// its handle so the backfilled frame fills the same slot in place.
2687    fn emit_gap_placeholder(&mut self, key: StreamKey, id: u8, epoch: u16) -> MessageHandle {
2688        let handle = self.alloc_handle();
2689        self.emit_mutation(
2690            handle,
2691            MutationKind::Insert {
2692                conversation: key.conversation,
2693                sender: key.sender,
2694                direction: Direction::Inbound,
2695                message_type: MessageType::Basic,
2696                wire_id: Some(id),
2697                epoch,
2698                client_token: None,
2699                sender_handle: None,
2700                regarding: None,
2701                bg_color: None,
2702                text_color: None,
2703                body: BodyRef { offset: 0, len: 0 },
2704                status: CompletionStatus::Complete,
2705                presence: Presence::GapPending,
2706                late: false,
2707                notify: false,
2708            },
2709        );
2710        if let Some(stream) = self.inbound.get_mut(&key) {
2711            stream.refs.record(id, handle);
2712        }
2713        handle
2714    }
2715
2716    /// An edit arrived whose target is a still-missing message with a
2717    /// reserved gap slot. The edit *is* that slot's current content: fill the
2718    /// placeholder with it (or remove the placeholder for a delete), cancel
2719    /// the pending repair, and account for the original ID so the superseded
2720    /// original — should it still arrive — is dropped as a duplicate instead
2721    /// of overwriting the newer content. Returns whether a slot was filled.
2722    fn fill_gap_with_edit(
2723        &mut self,
2724        envelope: &Envelope,
2725        key: StreamKey,
2726        original_id: u8,
2727        body: &[u8],
2728    ) -> bool {
2729        let Some(placeholder) = self.inbound.get(&key).and_then(|stream| {
2730            stream
2731                .pending
2732                .iter()
2733                .find(|pending| pending.message_id == original_id && pending.fragment.is_none())
2734                .and_then(|pending| pending.handle)
2735        }) else {
2736            return false;
2737        };
2738
2739        if let Some(stream) = self.inbound.get_mut(&key) {
2740            stream.cancel_pending(original_id, None);
2741            stream.seen.insert(original_id);
2742        }
2743        self.push_output(Output::Event(Event::RepairFinished {
2744            conversation: key.conversation,
2745            sender: key.sender,
2746            message_id: original_id,
2747            outcome: RepairOutcome::Repaired,
2748        }));
2749
2750        if body.is_empty() {
2751            // The missing message was deleted; its slot simply goes away.
2752            self.emit_mutation(
2753                placeholder,
2754                MutationKind::Delete {
2755                    conversation: key.conversation,
2756                    original: ResolvedRef::Handle(placeholder),
2757                },
2758            );
2759            return true;
2760        }
2761
2762        let text = core::str::from_utf8(body).unwrap_or("");
2763        let body_ref = self
2764            .arena_store(text)
2765            .unwrap_or(BodyRef { offset: 0, len: 0 });
2766        let epoch = self
2767            .inbound
2768            .get(&key)
2769            .map(|stream| stream.epoch)
2770            .unwrap_or(0);
2771        self.emit_mutation(
2772            placeholder,
2773            MutationKind::Insert {
2774                conversation: envelope.conversation,
2775                sender: envelope.sender,
2776                direction: Direction::Inbound,
2777                message_type: MessageType::Basic,
2778                wire_id: Some(original_id),
2779                epoch,
2780                client_token: None,
2781                sender_handle: None,
2782                regarding: None,
2783                bg_color: None,
2784                text_color: None,
2785                body: body_ref,
2786                status: CompletionStatus::Complete,
2787                presence: Presence::Present,
2788                late: true,
2789                notify: true,
2790            },
2791        );
2792        // The slot shows edited content; mark it as such.
2793        self.emit_mutation(
2794            placeholder,
2795            MutationKind::Edit {
2796                conversation: key.conversation,
2797                original: ResolvedRef::Handle(placeholder),
2798                body: body_ref,
2799            },
2800        );
2801        true
2802    }
2803
2804    /// Flip a still-outstanding gap placeholder to `Unavailable`: the repair
2805    /// was exhausted, expired, disclaimed, or abandoned. The row stays in
2806    /// place as a visible loss marker rather than silently vanishing.
2807    fn flip_placeholder_unavailable(&mut self, key: StreamKey, id: u8, handle: MessageHandle) {
2808        let epoch = self
2809            .inbound
2810            .get(&key)
2811            .map(|stream| stream.epoch)
2812            .unwrap_or(0);
2813        self.emit_mutation(
2814            handle,
2815            MutationKind::Insert {
2816                conversation: key.conversation,
2817                sender: key.sender,
2818                direction: Direction::Inbound,
2819                message_type: MessageType::Basic,
2820                wire_id: Some(id),
2821                epoch,
2822                client_token: None,
2823                sender_handle: None,
2824                regarding: None,
2825                bg_color: None,
2826                text_color: None,
2827                body: BodyRef { offset: 0, len: 0 },
2828                status: CompletionStatus::Complete,
2829                presence: Presence::Unavailable,
2830                late: false,
2831                notify: false,
2832            },
2833        );
2834        if let Some(stream) = self.inbound.get_mut(&key) {
2835            stream.refs.retire(id);
2836        }
2837    }
2838
2839    fn emit_mutation(&mut self, handle: MessageHandle, kind: MutationKind) {
2840        let revision = self.revision;
2841        self.revision = self.revision.wrapping_add(1);
2842        self.push_output(Output::StoreMessage(MessageMutation {
2843            handle,
2844            revision,
2845            kind,
2846        }));
2847    }
2848
2849    fn push_output(&mut self, output: Output) {
2850        if self.outputs.push_back(output).is_err() {
2851            self.lost_outputs = self.lost_outputs.wrapping_add(1);
2852        }
2853    }
2854
2855    fn arena_store(&mut self, text: &str) -> Option<BodyRef> {
2856        let bytes = text.as_bytes();
2857        if self.arena_used + bytes.len() > ARENA_SIZE || bytes.len() > u16::MAX as usize {
2858            self.push_output(Output::Diagnostic(Diagnostic::OutputOverflow));
2859            return None;
2860        }
2861        let offset = self.arena_used;
2862        self.arena[offset..offset + bytes.len()].copy_from_slice(bytes);
2863        self.arena_used += bytes.len();
2864        Some(BodyRef {
2865            offset: offset as u16,
2866            len: bytes.len() as u16,
2867        })
2868    }
2869
2870    /// Render a slot into the arena, returning the body reference.
2871    fn render_to_arena(&mut self, slot_index: usize, final_render: bool) -> BodyRef {
2872        let mut scratch = [0u8; REASSEMBLED_BODY_MAX + 64];
2873        let result = fragment::render_slot(
2874            &self.pool,
2875            slot_index,
2876            &self.config.sentinels,
2877            final_render,
2878            &mut scratch,
2879        );
2880        if result.complete && result.had_invalid {
2881            // The spec validates UTF-8 only once every fragment is present;
2882            // a complete body that fails is rendered lossily and reported.
2883            let message_id = self.pool.slots[slot_index]
2884                .as_ref()
2885                .expect("occupied")
2886                .message_id;
2887            self.push_output(Output::Diagnostic(Diagnostic::ReassembledInvalidUtf8 {
2888                message_id,
2889            }));
2890        }
2891        let text = core::str::from_utf8(&scratch[..result.len]).unwrap_or("");
2892        self.arena_store(text)
2893            .unwrap_or(BodyRef { offset: 0, len: 0 })
2894    }
2895}
2896
2897/// Delivery mode of a conversation, used for original sends and for resend
2898/// responses (which return on the conversation's mode, not the request's
2899/// arrival path).
2900pub fn destination_for(conversation: &ConversationKey) -> Destination {
2901    match conversation {
2902        ConversationKey::Direct { peer } => Destination::Peer(*peer),
2903        ConversationKey::Room { room } => Destination::Peer(*room),
2904        ConversationKey::ChannelGroup { channel } => Destination::Channel(*channel),
2905        ConversationKey::ChannelDirect { channel, peer } => Destination::ChannelPeer {
2906            channel: *channel,
2907            peer: *peer,
2908        },
2909    }
2910}