Skip to main content

forge_workspace/
update_fanout.rs

1//! Fan-out for the workspace's [`SessionUpdate`] stream.
2
3use std::collections::{HashMap, HashSet, VecDeque};
4use std::sync::Arc;
5
6use parking_lot::Mutex;
7use tokio::sync::mpsc;
8
9use crate::SessionSlot;
10use crate::protocol::SessionUpdate;
11
12/// One frame on the stream: the update, and the number the seat's own
13/// counter minted for it.
14///
15/// **The number is what makes a miss detectable.** Every frame the workspace
16/// emits for a seat is numbered on that seat's counter - whichever site
17/// emitted it - so a client that reads 5 and then 7 knows exactly one frame
18/// went missing and can ask for it. `seq` is `None` for a frame belonging to
19/// no seat, which rides the stream without joining any sequence.
20#[derive(Clone, Debug)]
21pub struct SeatUpdate {
22    pub seq: Option<u64>,
23    /// **Shared rather than owned, so one delivery is one pointer.** A frame
24    /// goes to every subscriber of the fleet, and each of them filters it
25    /// afterwards - so a deep clone per subscriber paid for a payload most of
26    /// them drop. The ring holds the same pointer.
27    pub update: Arc<SessionUpdate>,
28}
29
30/// How many frames one seat's stream keeps for repair.
31///
32/// **Sized from the window a client is served from**, like the conversation
33/// copy the transport holds: it is the stretch a repair is answered from
34/// memory, and what falls past it is the transcript's own read. A ring is shed
35/// whole at an occupant change - the numbers start over, so the frames before
36/// one are unreachable.
37const SEAT_RING: usize = crate::conversation_window::CONVERSATION_CAP;
38
39/// The byte ceiling on one seat's ring: the same 64 MiB the conversation copy
40/// beside it keeps ([`crate::conversation_window::CONVERSATION_CAP_BYTES`]).
41///
42/// **A ring exists to fill a copy that is behind**, so a ring allowed to weigh
43/// more than the copy it repairs bounds nothing a reader feels. The count alone
44/// left a released seat (a despawned worker, a lead closed for the day) pinning
45/// up to the full frame count for the process's life, and one
46/// `FileIndexChanged` frame holds a whole tree's index: at a hundred thousand
47/// files that is 15 MB per retained frame.
48const SEAT_RING_BYTES: usize = crate::conversation_window::CONVERSATION_CAP_BYTES;
49
50/// Whether the frame is kept in its seat's ring for repair.
51///
52/// **The frames that open or close an epoch stay out.** They carry the count
53/// of the run they end - and the history the epoch hands over - so a ring that
54/// kept one would report a front number the new run cannot answer for.
55fn ringed(update: &SessionUpdate) -> bool {
56    !matches!(
57        update,
58        SessionUpdate::Connected { .. }
59            | SessionUpdate::SessionReplaced { .. }
60            | SessionUpdate::Releasing { .. }
61    )
62}
63
64/// What a frame costs against [`SEAT_RING_BYTES`]: the bytes its own JSON
65/// would write, plus the envelope around it.
66///
67/// **Measured, not hand-listed.** A walk over the fields that can grow charged
68/// only the variants it named: a frame outside that list, and a field added to
69/// one inside it, cost a fixed weight however large it was - so a new field
70/// silently under-charged the ceiling that exists to bound memory. The count is
71/// taken through a sink that keeps nothing, so the number is the memory
72/// without a second copy of it.
73fn frame_bytes(update: &SessionUpdate) -> usize {
74    /// The frame's own envelope: its tag, slot, seq and span.
75    const FRAME: usize = 256;
76    let mut count = ByteCount(0);
77    match serde_json::to_writer(&mut count, update) {
78        Ok(()) => count.0.saturating_add(FRAME),
79        // A payload that cannot serialise is a defect the wire would show
80        // anyway; charge it the envelope rather than nothing.
81        Err(_) => FRAME,
82    }
83}
84
85/// A `Write` that keeps the number of bytes written and nothing else.
86struct ByteCount(usize);
87
88impl std::io::Write for ByteCount {
89    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
90        self.0 = self.0.saturating_add(buf.len());
91        Ok(buf.len())
92    }
93
94    fn flush(&mut self) -> std::io::Result<()> {
95        Ok(())
96    }
97}
98
99/// One frame in a seat's ring, with the weight it was charged when it was
100/// minted: eviction subtracts that rather than weighing the frame again
101/// under the fan-out's lock.
102struct Kept {
103    frame: SeatUpdate,
104    bytes: usize,
105}
106
107/// Keep one unringed number, bounded to a ring's frame count - the stretch
108/// beyond which no window a fill could serve reaches.
109fn remember(numbers: &mut VecDeque<u64>, seq: u64, cap: usize) {
110    numbers.push_back(seq);
111    while numbers.len() > cap {
112        numbers.pop_front();
113    }
114}
115
116/// What a subscriber can do with what it is sent.
117#[derive(Clone, Copy, PartialEq, Eq)]
118pub(crate) enum SubscriberRole {
119    /// Renders the workspace's prompts - permission, question, Slack
120    /// draft - and answers them. The answering view's role, and the one
121    /// [`UpdateFanout::send_answering`] reports on.
122    Answering,
123    /// Reads the stream only. A prompt delivered to observers alone is
124    /// one nobody will answer, so a path that parks a turn on an answer
125    /// must not count this subscriber.
126    Observing,
127}
128
129/// One subscriber's end of the fan-out.
130///
131/// **The channel is unbounded, and what bounds it instead is the connection.**
132/// A bounded channel forces the sender to choose between blocking - which
133/// would hold every seat's stream behind one slow reader, under this fan-out's
134/// own lock - and dropping a frame, which rule 25 forbids. So the sender never
135/// waits, and the receiving connection drains continuously: a socket that
136/// stops accepting its frames is cut at `WRITE_BOUND` (60 s), so a subscriber
137/// that keeps up holds a batch's worth and one that does not is disconnected
138/// rather than queued against forever. The per-frame weight is a pointer now,
139/// shared with the ring below rather than copied per subscriber.
140struct Registration {
141    tx: mpsc::UnboundedSender<SeatUpdate>,
142    role: SubscriberRole,
143}
144
145struct Shared {
146    subscribers: Vec<Registration>,
147    /// Where each seat's stream has got to: the number its newest frame was
148    /// minted under. Absent for a seat that has not emitted yet, and for one
149    /// whose last frame opened a new epoch.
150    ///
151    /// **Per seat, keyed by the frame's own slot**, so a frame for one seat
152    /// can neither advance another's counter nor leave a hole in it.
153    sequences: HashMap<SessionSlot, u64>,
154    /// The newest frames each seat's stream has emitted, oldest first, for a
155    /// client that read a gap in the numbers and asks for what it missed.
156    ///
157    /// Held here rather than on the session task because here is where every
158    /// frame of the seat crosses, whichever site emitted it - and the number a
159    /// repair hands back is the one minted here.
160    rings: HashMap<SessionSlot, VecDeque<Kept>>,
161    /// What each seat's ring weighs, beside the frames themselves, so the
162    /// byte ceiling is a running total rather than a walk of the ring.
163    ring_bytes: HashMap<SessionSlot, usize>,
164    /// The numbers each seat has minted that the ring does NOT hold: the
165    /// frames that close an epoch. A reader measuring whether a copy covers a
166    /// stretch needs them, because a window can never serve them and a gap
167    /// holding only one of them costs the copy nothing.
168    ///
169    /// **Per epoch, not per seat.** An occupant change mints from 1 again, so
170    /// the numbers a seal's epoch carried are re-minted by the run after it -
171    /// and the record of them goes with the ring. Bounded to a ring's frame
172    /// count: a number older than that lies beyond any window a fill could
173    /// serve, where the count it underwrites is a refusal rather than a hole.
174    unringed: HashMap<SessionSlot, VecDeque<u64>>,
175    /// The seats whose latest epoch-opening seal carried rows.
176    ///
177    /// **A seal's history is not in the ring**, so a reader filling a copy
178    /// from the window cannot rebuild what the epoch opened on (#2446). An
179    /// empty-history seal opens its epoch with nothing to miss, so each mint
180    /// REPLACES this rather than adding to it. The value standing between an
181    /// occupant change and its seal's mint is the previous seal's, which is
182    /// what notices that gap.
183    sealed_with_rows: HashSet<SessionSlot>,
184    /// How many frames a ring keeps. [`SEAT_RING`] in production; a test
185    /// drives it down so the transcript fallback is reachable without
186    /// emitting thousands of frames.
187    ring_cap: usize,
188    /// How many bytes a ring keeps. [`SEAT_RING_BYTES`] in production; a test
189    /// drives it down so the ceiling is reachable without a giant tree.
190    ring_cap_bytes: usize,
191}
192
193impl Default for Shared {
194    fn default() -> Self {
195        Self {
196            subscribers: Vec::new(),
197            sequences: HashMap::new(),
198            rings: HashMap::new(),
199            ring_bytes: HashMap::new(),
200            unringed: HashMap::new(),
201            sealed_with_rows: HashSet::new(),
202            ring_cap: SEAT_RING,
203            ring_cap_bytes: SEAT_RING_BYTES,
204        }
205    }
206}
207
208impl Shared {
209    /// Number `update` on its seat's counter, or leave it unnumbered when it
210    /// belongs to no seat.
211    ///
212    /// **An occupant change closes its epoch.** `Connected` and
213    /// `SessionReplaced` are numbered on the run they end and the seat's
214    /// counter starts over, so the frame after one opens the new epoch at 1:
215    /// the client learns the occupant from that frame itself and wipes to
216    /// expect 1, never a gap.
217    ///
218    /// **One lock serializes numbering and delivery.** [`UpdateFanout::deliver`]
219    /// mints and fans out under the same guard, so the numbers a seat's
220    /// subscribers receive are in the order they were minted. Lifting the mint
221    /// out of that guard - or fanning out around it - reorders a seat's stream
222    /// against its own numbers.
223    fn numbered(&mut self, update: SessionUpdate, charge: usize) -> SeatUpdate {
224        let Some(slot) = update.slot().cloned() else {
225            return SeatUpdate { seq: None, update: Arc::new(update) };
226        };
227        let seq = self.next_seq(&slot);
228        let epoch_frame = !ringed(&update);
229        if matches!(
230            &update,
231            SessionUpdate::Connected { .. } | SessionUpdate::SessionReplaced { .. }
232        ) {
233            self.sequences.remove(&slot);
234            // **A new occupant starts counting again**, so the frames before
235            // it belong to a stream no client holds any more - and a repair
236            // that could reach into them would splice two conversations. The
237            // unringed numbers are the same: the run after a seal mints them
238            // again, and a stale one would cover a gap the new run opened.
239            self.rings.remove(&slot);
240            self.ring_bytes.remove(&slot);
241            self.unringed.remove(&slot);
242            // **The mark describes the LATEST seal, so each mint replaces
243            // it.** Only a seal that carried rows leaves a gap the ring cannot
244            // fill, and a later blank epoch must not inherit the previous
245            // one's mark: its ring was cleared at this mint, so a fill holds
246            // its whole history. Before the mint the value is still the
247            // previous seal's, which is what notices the header-move window.
248            let carried = match &update {
249                SessionUpdate::Connected { history, .. }
250                | SessionUpdate::SessionReplaced { history, .. } => !history.is_empty(),
251                _ => false,
252            };
253            if carried {
254                self.sealed_with_rows.insert(slot.clone());
255            } else {
256                self.sealed_with_rows.remove(&slot);
257            }
258        } else if matches!(update, SessionUpdate::Releasing { .. }) {
259            // **The counter still advances** - the frame carries a number like
260            // any other, and leaving the counter behind mints that number
261            // again for the frame after it, which a client reads as one
262            // already drawn.
263            //
264            // **The ring stays.** A seat a connection re-opens after its
265            // session ended is answered from its record, and that read can be
266            // behind frames this release ends; shedding the ring would refuse
267            // it with a hole no window could fill. It is shed at the next
268            // occupant's seal, which is where the numbers start over.
269            self.sequences.insert(slot.clone(), seq);
270            remember(self.unringed.entry(slot.clone()).or_default(), seq, self.ring_cap);
271        } else {
272            self.sequences.insert(slot.clone(), seq);
273        }
274        let frame = SeatUpdate { seq: Some(seq), update: Arc::new(update) };
275        if !epoch_frame {
276            let ring = self.rings.entry(slot.clone()).or_default();
277            ring.push_back(Kept { frame: frame.clone(), bytes: charge });
278            let bytes = self.ring_bytes.entry(slot).or_default();
279            *bytes = bytes.saturating_add(charge);
280            // Two ceilings, one cut: a giant index frame must not survive on
281            // the count alone, and a long run of small frames must not survive
282            // on the byte ceiling alone.
283            while ring.len() > 1 && (ring.len() > self.ring_cap || *bytes > self.ring_cap_bytes) {
284                let Some(old) = ring.pop_front() else {
285                    break;
286                };
287                *bytes = bytes.saturating_sub(old.bytes);
288            }
289        }
290        frame
291    }
292
293    /// The frames of `slot`'s ring that carry the numbers in `from..=to`,
294    /// oldest first.
295    fn frames_between(&self, slot: &SessionSlot, from: u64, to: u64) -> Vec<SeatUpdate> {
296        self.rings
297            .get(slot)
298            .map(|ring| {
299                ring.iter()
300                    .map(|kept| &kept.frame)
301                    .filter(|frame| frame.seq.is_some_and(|seq| (from..=to).contains(&seq)))
302                    .cloned()
303                    .collect()
304            })
305            .unwrap_or_default()
306    }
307
308    /// The oldest number `slot`'s ring still holds, `None` for a seat that
309    /// has emitted nothing.
310    fn oldest_kept(&self, slot: &SessionSlot) -> Option<u64> {
311        self.rings.get(slot).and_then(|ring| ring.front()).and_then(|kept| kept.frame.seq)
312    }
313
314    /// How many of the numbers in `from..=to` on `slot`'s stream the ring
315    /// holds no frame for.
316    fn unringed_between(&self, slot: &SessionSlot, from: u64, to: u64) -> u64 {
317        let Some(numbers) = self.unringed.get(slot) else {
318            return 0;
319        };
320        u64::try_from(numbers.iter().filter(|number| (from..=to).contains(number)).count())
321            .unwrap_or(u64::MAX)
322    }
323
324    /// The number `slot`'s next frame will be minted under.
325    ///
326    /// A seat that has emitted nothing - and one whose last frame opened a new
327    /// epoch - is at 1, so this never answers 0: a client reads it as the
328    /// number to expect, and 0 would have it expect a frame that cannot exist.
329    fn next_seq(&self, slot: &SessionSlot) -> u64 {
330        self.sequences.get(slot).copied().unwrap_or(0) + 1
331    }
332
333    /// Whether `slot`'s latest epoch-opening seal carried rows.
334    fn epoch_opened_with_rows(&self, slot: &SessionSlot) -> bool {
335        self.sealed_with_rows.contains(slot)
336    }
337}
338
339/// Cloneable sender half of the workspace's fan-out to its subscribers.
340///
341/// Every clone shares one registry. [`Self::send`] hands the update to
342/// each subscriber and drops any whose receiver has gone, so the
343/// registry never keeps a dead one.
344#[derive(Clone, Default)]
345pub(crate) struct UpdateFanout {
346    shared: Arc<Mutex<Shared>>,
347}
348
349impl UpdateFanout {
350    /// Mint a subscriber stream, declaring what the caller can do with
351    /// it. A stream carries what is emitted after this call and nothing
352    /// before it; a subscriber reads what it missed from the subject's
353    /// snapshot instead.
354    pub(crate) fn subscribe(&self, role: SubscriberRole) -> mpsc::UnboundedReceiver<SeatUpdate> {
355        let (tx, rx) = mpsc::unbounded_channel();
356        self.shared.lock().subscribers.push(Registration { tx, role });
357        rx
358    }
359
360    /// Deliver `update` to every subscriber, and report whether one took
361    /// it. `false` is the "no receiver" signal the single-take channel
362    /// gave, which `spawn::try_emit` and `SessionTask::emit` log on.
363    ///
364    /// **One update, one pointer.** The frame is wrapped once and each
365    /// subscriber is handed an `Arc` clone, so the fan-out never pays a
366    /// payload copy however many subscribers there are. The last subscriber
367    /// is taken out of the registry to receive it directly and goes back only
368    /// if it took it, which is also where a dead one leaves.
369    pub(crate) fn send(&self, update: SessionUpdate) -> bool {
370        self.deliver(update, None)
371    }
372
373    /// The number `slot`'s next frame will be minted under.
374    ///
375    /// The read behind a seat's reconnect answer: the occupant names the
376    /// epoch and this names where its stream stands, so a client that has
377    /// just read the seat knows exactly what to expect next.
378    pub(crate) fn next_seq(&self, slot: &SessionSlot) -> u64 {
379        self.shared.lock().next_seq(slot)
380    }
381
382    /// The frames of `slot`'s window numbered in `from..=to`, oldest first,
383    /// for a client that read a gap in them.
384    pub(crate) fn frames_between(&self, slot: &SessionSlot, from: u64, to: u64) -> Vec<SeatUpdate> {
385        self.shared.lock().frames_between(slot, from, to)
386    }
387
388    /// The oldest number `slot`'s window still holds: what tells a range it
389    /// can answer from memory from one it cannot.
390    pub(crate) fn oldest_kept(&self, slot: &SessionSlot) -> Option<u64> {
391        self.shared.lock().oldest_kept(slot)
392    }
393
394    /// How many numbers in `from..=to` on `slot`'s stream no window can hold.
395    pub(crate) fn unringed_between(&self, slot: &SessionSlot, from: u64, to: u64) -> u64 {
396        self.shared.lock().unringed_between(slot, from, to)
397    }
398
399    /// Whether `slot`'s latest epoch-opening seal carried rows, which no ring
400    /// holds.
401    pub(crate) fn epoch_opened_with_rows(&self, slot: &SessionSlot) -> bool {
402        self.shared.lock().epoch_opened_with_rows(slot)
403    }
404
405    /// Drive the ring's cap down, so a test reaches the transcript fallback
406    /// without emitting a window's worth of frames.
407    #[cfg(any(test, feature = "testing"))]
408    pub(crate) fn set_ring_cap(&self, cap: usize) {
409        self.shared.lock().ring_cap = cap;
410    }
411
412    /// Drive the ring's byte ceiling down, so a test reaches it with frames a
413    /// fixture can build rather than with a giant tree.
414    #[cfg(test)]
415    pub(crate) fn set_ring_cap_bytes(&self, bytes: usize) {
416        self.shared.lock().ring_cap_bytes = bytes;
417    }
418
419    /// What `slot`'s ring weighs, for the tests that pin its ceiling.
420    #[cfg(test)]
421    pub(crate) fn ring_bytes(&self, slot: &SessionSlot) -> usize {
422        self.shared.lock().ring_bytes.get(slot).copied().unwrap_or(0)
423    }
424
425    /// How many subscribers are attached.
426    ///
427    /// A test uses this to tell one subscription from another. "Did anyone
428    /// receive" cannot: a listener that is always there - the socket's own
429    /// fold - makes every emit land, however many views have come and gone.
430    #[cfg(any(test, feature = "testing"))]
431    pub(crate) fn count(&self) -> usize {
432        self.shared.lock().subscribers.len()
433    }
434
435    /// How many subscribers could answer a prompt. Test-only: the role decides
436    /// whether the core parks a turn on a subscriber's reply, and nothing else
437    /// reports it.
438    #[cfg(any(test, feature = "testing"))]
439    pub(crate) fn answering_count(&self) -> usize {
440        self.shared
441            .lock()
442            .subscribers
443            .iter()
444            .filter(|held| held.role == SubscriberRole::Answering)
445            .count()
446    }
447
448    /// Deliver `update` to every subscriber, and report whether one that
449    /// can ANSWER it took it. A path that parks a turn on an answer uses
450    /// this rather than [`Self::send`]: an observer takes the update and
451    /// never replies, so counting it would leave the turn waiting on a
452    /// response nobody will send.
453    ///
454    /// An update emitted before anything attached reaches nobody: a
455    /// subscriber takes what follows its own subscribe. So a caller must
456    /// fail its turn closed on a `false` return; one that parked the
457    /// turn anyway belongs on [`Self::send`].
458    pub(crate) fn send_answering(&self, update: SessionUpdate) -> bool {
459        self.deliver(update, Some(SubscriberRole::Answering))
460    }
461
462    fn deliver(&self, update: SessionUpdate, required: Option<SubscriberRole>) -> bool {
463        // **Weighed before the lock, and only when it is kept.** A chat
464        // frame's charge is its JSON, and paying that inside the fan-out's
465        // guard would serialize every seat's stream behind it; an epoch frame
466        // is never ringed, and its history makes that the one serialisation
467        // worth skipping.
468        let charge = if ringed(&update) { frame_bytes(&update) } else { 0 };
469        let mut shared = self.shared.lock();
470        let update = shared.numbered(update, charge);
471        let Some(tail) = shared.subscribers.pop() else {
472            return false;
473        };
474        let counts = |role: SubscriberRole| required.is_none_or(|want| want == role);
475        let mut took_it = false;
476        shared.subscribers.retain(|subscriber| {
477            let live = subscriber
478                .tx
479                .send(SeatUpdate { seq: update.seq, update: Arc::clone(&update.update) })
480                .is_ok();
481            took_it |= live && counts(subscriber.role);
482            live
483        });
484        if tail.tx.send(update).is_ok() {
485            took_it |= counts(tail.role);
486            shared.subscribers.push(tail);
487        }
488        took_it
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use forge_primitives::SessionId;
495    use forge_primitives::cloud::service_status::ServiceSeverity;
496    use forge_primitives::runtime::CurrentModel;
497
498    use super::*;
499    use crate::SessionSlot;
500    use crate::protocol::NoticeSeverity;
501
502    fn status(message: &str) -> SessionUpdate {
503        SessionUpdate::ServiceStatus {
504            severity: ServiceSeverity::Warning,
505            message: message.to_owned(),
506        }
507    }
508
509    fn seat(label: &str) -> SessionSlot {
510        SessionSlot::from_str_for_test(label)
511    }
512
513    /// A frame the seat emits. Any of them will do, and this one is the
514    /// cheapest to build.
515    fn a_turn_ends(key: &SessionSlot) -> SessionUpdate {
516        SessionUpdate::TurnComplete { key: key.clone(), terminal_reason: None }
517    }
518
519    /// The seat's occupant changing, which is the frame an epoch turns on.
520    fn occupant_change(key: &SessionSlot) -> SessionUpdate {
521        SessionUpdate::SessionReplaced {
522            key: key.clone(),
523            session_id: SessionId("a-new-occupant".to_owned()),
524            cwd: "/tmp".to_owned(),
525            current_model: CurrentModel::new("m", "m", "m"),
526            available_models: Vec::new(),
527            mode: None,
528            history: Vec::new(),
529            dropped: 0,
530            compaction_count: 0,
531        }
532    }
533
534    /// A `FileIndexChanged` of `entries` files, for the tests that drive the
535    /// ring's byte ceiling: the variant that pins a whole tree per frame.
536    fn a_big_index(key: &SessionSlot, entries: usize) -> SessionUpdate {
537        let mut index = crate::file_index::FileIndex::default();
538        for nth in 0..entries {
539            let rel = format!("src/deeply/nested/module/number-{nth:04}/file-{nth:04}.rs");
540            index.entries.insert(
541                rel.clone(),
542                crate::file_index::FileCandidate {
543                    rel_path: rel.clone(),
544                    rel_path_lower: rel.to_lowercase(),
545                    basename_lower: rel.to_lowercase(),
546                    depth: 5,
547                },
548            );
549        }
550        SessionUpdate::FileIndexChanged { key: key.clone(), index: Arc::new(index) }
551    }
552
553    /// The next frame on `who`'s stream, unread.
554    fn frame(rx: &mut mpsc::UnboundedReceiver<SeatUpdate>, who: &str) -> SeatUpdate {
555        rx.try_recv().unwrap_or_else(|err| {
556            panic!("expected an update on the {who} subscriber's stream: {err}")
557        })
558    }
559
560    /// The next frame's number, for the tests that read a seat's sequence.
561    fn next_seq(rx: &mut mpsc::UnboundedReceiver<SeatUpdate>, who: &str) -> Option<u64> {
562        frame(rx, who).seq
563    }
564
565    fn said(frame: SeatUpdate) -> String {
566        match std::sync::Arc::unwrap_or_clone(frame.update) {
567            SessionUpdate::ServiceStatus { message, .. } => message,
568            other => panic!("expected the ServiceStatus envelope this test sends, got {other:?}"),
569        }
570    }
571
572    fn next(rx: &mut mpsc::UnboundedReceiver<SeatUpdate>, who: &str) -> String {
573        said(frame(rx, who))
574    }
575
576    /// Catches `send` delivering to only the first subscriber, which is
577    /// the single-take behaviour this replaces.
578    #[test]
579    fn each_subscriber_receives_every_update() {
580        let fanout = UpdateFanout::default();
581        let mut first = fanout.subscribe(SubscriberRole::Answering);
582        let mut second = fanout.subscribe(SubscriberRole::Answering);
583
584        assert!(fanout.send(status("one")), "a subscribed fan-out delivers");
585
586        assert_eq!(next(&mut first, "first"), "one", "the first subscriber sees the update");
587        assert_eq!(
588            next(&mut second, "second"),
589            "one",
590            "the second subscriber sees the same update"
591        );
592    }
593
594    /// Catches dropping the prune in `send`, which leaves a dead sender
595    /// in the registry for the life of the workspace. The dropped
596    /// subscriber is subscribed FIRST, so it is not the one `send` takes
597    /// out as the tail and the prune is the only thing that can remove
598    /// it.
599    #[test]
600    fn a_subscriber_that_drops_leaves_the_fan_out() {
601        let fanout = UpdateFanout::default();
602        let dropped = fanout.subscribe(SubscriberRole::Answering);
603        let mut kept = fanout.subscribe(SubscriberRole::Answering);
604        drop(dropped);
605
606        assert!(fanout.send(status("one")), "the live subscriber still receives");
607
608        assert_eq!(
609            fanout.shared.lock().subscribers.len(),
610            1,
611            "the dropped subscriber is gone from the registry",
612        );
613        assert_eq!(next(&mut kept, "kept"), "one", "the live subscriber still receives");
614    }
615
616    /// Catches buffering emitted updates and replaying them to the next
617    /// subscriber, which hands a view a backlog it did not ask for.
618    #[test]
619    fn a_late_subscriber_sees_only_what_follows_it() {
620        let fanout = UpdateFanout::default();
621        let mut first = fanout.subscribe(SubscriberRole::Answering);
622        assert!(fanout.send(status("before")), "the first subscriber receives");
623
624        let mut late = fanout.subscribe(SubscriberRole::Answering);
625        assert!(fanout.send(status("after")), "both subscribers receive");
626
627        assert_eq!(
628            next(&mut first, "first"),
629            "before",
630            "the first subscriber keeps its earlier update"
631        );
632        assert_eq!(
633            next(&mut first, "first"),
634            "after",
635            "the first subscriber also sees the later one"
636        );
637        assert_eq!(next(&mut late, "late"), "after", "the late subscriber sees only the later one");
638        assert!(late.try_recv().is_err(), "the late subscriber carries nothing from before it");
639    }
640
641    /// **A delivery hands out the same update, not a copy of it.** The ring
642    /// and every subscriber hold one `Arc`: a payload nobody mutates is
643    /// shared, and a frame a subscriber filters out costs it a pointer rather
644    /// than a clone of a window.
645    #[test]
646    fn a_delivery_shares_one_update_with_the_ring_and_every_subscriber() {
647        let fanout = UpdateFanout::default();
648        let mut first = fanout.subscribe(SubscriberRole::Answering);
649        let mut second = fanout.subscribe(SubscriberRole::Answering);
650        let seat = seat("lead");
651
652        assert!(fanout.send(a_turn_ends(&seat)), "both subscribers receive it");
653        let one = first.try_recv().expect("the first subscriber's frame");
654        let two = second.try_recv().expect("the second subscriber's frame");
655        assert!(
656            Arc::ptr_eq(&one.update, &two.update),
657            "the two subscribers were handed one payload, not two",
658        );
659        let ringed = fanout.frames_between(&seat, 1, 1);
660        assert_eq!(ringed.len(), 1, "the ring kept the frame");
661        assert!(
662            Arc::ptr_eq(&one.update, &ringed[0].update),
663            "and the ring holds the same payload rather than a second copy",
664        );
665    }
666
667    /// Catches pointing a guard at `send`, which counts an observer as an
668    /// answer: the update reaches the observer, but the turn that raised
669    /// it would wait forever on a reply an observer never sends.
670    #[test]
671    fn an_observer_takes_the_update_without_answering_it() {
672        let fanout = UpdateFanout::default();
673        let mut observer = fanout.subscribe(SubscriberRole::Observing);
674
675        assert!(!fanout.send_answering(status("one")), "an observer does not answer");
676        assert_eq!(next(&mut observer, "observer"), "one", "the observer still sees the update");
677
678        let mut answering = fanout.subscribe(SubscriberRole::Answering);
679        assert!(fanout.send_answering(status("two")), "an answering subscriber answers");
680        assert_eq!(next(&mut answering, "answering"), "two", "it sees the update too");
681    }
682
683    /// Catches `send` swallowing the no-subscriber case, which silences
684    /// the "receiver is gone" diagnostics at both emit sites.
685    #[test]
686    fn send_without_a_subscriber_reports_nothing_delivered() {
687        let fanout = UpdateFanout::default();
688
689        assert!(!fanout.send(status("one")), "an unsubscribed fan-out delivers to nobody");
690    }
691
692    /// A seat's frames are numbered on that seat's own counter, 1 then 2.
693    #[test]
694    fn a_seats_frames_carry_their_own_numbers() {
695        let fanout = UpdateFanout::default();
696        let mut rx = fanout.subscribe(SubscriberRole::Answering);
697        let seat = seat("lead");
698
699        assert!(fanout.send(a_turn_ends(&seat)), "a subscribed fan-out delivers");
700        assert!(fanout.send(a_turn_ends(&seat)), "a subscribed fan-out delivers");
701
702        assert_eq!(next_seq(&mut rx, "seat"), Some(1), "the seat's first frame is number 1");
703        assert_eq!(next_seq(&mut rx, "seat"), Some(2), "and its next is the successor");
704    }
705
706    /// An occupant change ends its epoch: the frame itself is the last of
707    /// the run it closes, and the next frame opens the new epoch at 1.
708    ///
709    /// Catches resetting before the frame is numbered, which leaves the
710    /// occupant frame carrying a number from the new epoch it opens.
711    #[test]
712    fn an_occupant_change_restarts_the_seats_numbers() {
713        let fanout = UpdateFanout::default();
714        let mut rx = fanout.subscribe(SubscriberRole::Answering);
715        let seat = seat("lead");
716
717        assert!(fanout.send(a_turn_ends(&seat)), "a subscribed fan-out delivers");
718        assert!(fanout.send(a_turn_ends(&seat)), "a subscribed fan-out delivers");
719        assert!(fanout.send(occupant_change(&seat)), "a subscribed fan-out delivers");
720
721        assert_eq!(next_seq(&mut rx, "seat"), Some(1), "the seat's first frame is number 1");
722        assert_eq!(next_seq(&mut rx, "seat"), Some(2), "and its next is the successor");
723        assert_eq!(
724            next_seq(&mut rx, "seat"),
725            Some(3),
726            "the occupant frame is numbered on the counter it ends",
727        );
728
729        assert!(fanout.send(a_turn_ends(&seat)), "a subscribed fan-out delivers");
730        assert_eq!(
731            next_seq(&mut rx, "seat"),
732            Some(1),
733            "and the frame after it opens the new epoch at 1",
734        );
735    }
736
737    /// The first `Connected` of a fresh seat is the same rule: the seat had
738    /// no occupant to number against, so the frame closes a run of one and
739    /// the epoch it opens starts at 1.
740    #[test]
741    fn a_fresh_connect_opens_its_epoch_at_one() {
742        let fanout = UpdateFanout::default();
743        let mut rx = fanout.subscribe(SubscriberRole::Answering);
744        let seat = seat("lead");
745
746        assert!(
747            fanout.send(SessionUpdate::Connected {
748                key: seat.clone(),
749                session_id: SessionId("the-first-occupant".to_owned()),
750                cwd: "/tmp".to_owned(),
751                current_model: CurrentModel::new("m", "m", "m"),
752                available_models: Vec::new(),
753                mode: None,
754                history: Vec::new(),
755                dropped: 0,
756                compaction_count: 0,
757            }),
758            "a subscribed fan-out delivers",
759        );
760
761        assert_eq!(next_seq(&mut rx, "seat"), Some(1), "the connect is the seat's first frame");
762
763        assert!(fanout.send(a_turn_ends(&seat)), "a subscribed fan-out delivers");
764        assert_eq!(
765            next_seq(&mut rx, "seat"),
766            Some(1),
767            "and the epoch it opens starts at 1, so the client wiping on it expects 1",
768        );
769    }
770
771    /// A frame that belongs to no seat rides unnumbered, and numbering it
772    /// would put a hole in every seat's sequence at once.
773    #[test]
774    fn a_frame_for_no_seat_rides_unnumbered() {
775        let fanout = UpdateFanout::default();
776        let mut rx = fanout.subscribe(SubscriberRole::Answering);
777        let seat = seat("lead");
778
779        assert!(fanout.send(status("boot")), "a subscribed fan-out delivers");
780        assert!(fanout.send(a_turn_ends(&seat)), "a subscribed fan-out delivers");
781
782        assert_eq!(
783            next_seq(&mut rx, "app-level"),
784            None,
785            "a workspace-level frame carries no number",
786        );
787        assert_eq!(
788            next_seq(&mut rx, "seat"),
789            Some(1),
790            "and it does not advance any seat's counter",
791        );
792    }
793
794    /// Two seats interleaved keep independent counters: a frame for another
795    /// seat must not advance this one's.
796    ///
797    /// Catches one counter for the whole fan-out, which is the reading that
798    /// makes a second seat's traffic look like a hole in the first's.
799    #[test]
800    fn two_seats_keep_independent_counters() {
801        let fanout = UpdateFanout::default();
802        let mut rx = fanout.subscribe(SubscriberRole::Answering);
803        let lead = seat("lead");
804        let worker = seat("worker");
805
806        assert!(fanout.send(a_turn_ends(&lead)), "a subscribed fan-out delivers");
807        assert!(fanout.send(a_turn_ends(&worker)), "a subscribed fan-out delivers");
808        assert!(fanout.send(a_turn_ends(&lead)), "a subscribed fan-out delivers");
809        assert!(fanout.send(a_turn_ends(&worker)), "a subscribed fan-out delivers");
810
811        assert_eq!(next_seq(&mut rx, "lead"), Some(1), "the lead's first frame is number 1");
812        assert_eq!(
813            next_seq(&mut rx, "worker"),
814            Some(1),
815            "the worker's own run opens at 1 rather than continuing the lead's",
816        );
817        assert_eq!(next_seq(&mut rx, "lead"), Some(2), "the lead's next is the successor");
818        assert_eq!(next_seq(&mut rx, "worker"), Some(2), "and so is the worker's");
819    }
820
821    /// **An occupant change leaves the ring holding only the new epoch.** The
822    /// frame that OPENS an epoch carries the count of the run it ends, so a
823    /// ring that kept it would report a front number the new run cannot answer
824    /// for, and the old number would collide with a new one as the count climbs
825    /// back to it. Repairs would fall to the transcript for frames the window
826    /// still holds.
827    #[test]
828    fn an_occupant_change_leaves_the_ring_on_the_new_epoch_alone() {
829        let fanout = UpdateFanout::default();
830        let mut rx = fanout.subscribe(SubscriberRole::Answering);
831        let seat = seat("lead");
832
833        assert!(fanout.send(a_turn_ends(&seat)), "a subscribed fan-out delivers");
834        assert!(fanout.send(a_turn_ends(&seat)), "a subscribed fan-out delivers");
835        assert!(fanout.send(occupant_change(&seat)), "a subscribed fan-out delivers");
836        assert!(fanout.send(a_turn_ends(&seat)), "a subscribed fan-out delivers");
837        assert!(fanout.send(a_turn_ends(&seat)), "a subscribed fan-out delivers");
838
839        assert_eq!(next_seq(&mut rx, "seat"), Some(1), "the old run's first frame");
840        assert_eq!(next_seq(&mut rx, "seat"), Some(2), "and its second");
841        assert_eq!(
842            next_seq(&mut rx, "seat"),
843            Some(3),
844            "the occupant frame is numbered on the counter it ends",
845        );
846        assert_eq!(next_seq(&mut rx, "seat"), Some(1), "and the new epoch opens at 1");
847        assert_eq!(next_seq(&mut rx, "seat"), Some(2), "counting on");
848
849        assert_eq!(
850            fanout.oldest_kept(&seat),
851            Some(1),
852            "the ring's front is the new epoch's first frame, not the frame that ended the old run",
853        );
854        assert_eq!(
855            fanout.frames_between(&seat, 1, 3).iter().map(|frame| frame.seq).collect::<Vec<_>>(),
856            vec![Some(1), Some(2)],
857            "and a range reaches only the new count's frames",
858        );
859    }
860
861    /// **A release advances the seat's counter like any other frame.** Keeping
862    /// the epoch frame out of the ring is where the branch began, and leaving
863    /// the counter behind with it mints the released frame's number twice: the
864    /// frame after it repeats a number a client has already drawn and is
865    /// skipped.
866    #[test]
867    fn a_release_advances_the_counter_it_is_numbered_on() {
868        let fanout = UpdateFanout::default();
869        let mut rx = fanout.subscribe(SubscriberRole::Answering);
870        let seat = seat("lead");
871
872        assert!(fanout.send(a_turn_ends(&seat)), "a subscribed fan-out delivers");
873        assert!(
874            fanout.send(SessionUpdate::Releasing { key: seat.clone() }),
875            "a subscribed fan-out delivers"
876        );
877        assert!(fanout.send(a_turn_ends(&seat)), "a subscribed fan-out delivers");
878
879        assert_eq!(next_seq(&mut rx, "seat"), Some(1), "the first frame");
880        assert_eq!(next_seq(&mut rx, "seat"), Some(2), "the release");
881        assert_eq!(
882            next_seq(&mut rx, "seat"),
883            Some(3),
884            "and the frame after it, rather than a number minted twice",
885        );
886    }
887
888    /// **The number a release is numbered under is one no ring will ever
889    /// hold.** A reader measuring whether a copy covers a stretch has to leave
890    /// that number out of the count, or every seat that closes reads as a copy
891    /// behind its own count - ready to be repaired from a window that cannot
892    /// hold the frame in question. A new epoch forgets the old release, because
893    /// the numbers after a seal are minted from 1 again.
894    #[test]
895    fn the_release_is_the_number_the_ring_cannot_serve() {
896        let fanout = UpdateFanout::default();
897        let mut rx = fanout.subscribe(SubscriberRole::Answering);
898        let seat = seat("lead");
899
900        assert!(fanout.send(a_turn_ends(&seat)), "a subscribed fan-out delivers");
901        assert!(
902            fanout.send(SessionUpdate::Releasing { key: seat.clone() }),
903            "a subscribed fan-out delivers"
904        );
905        assert_eq!(next_seq(&mut rx, "seat"), Some(1), "the row");
906        assert_eq!(next_seq(&mut rx, "seat"), Some(2), "and the release after it");
907
908        assert_eq!(
909            fanout.unringed_between(&seat, 1, 1),
910            0,
911            "a row the window holds is not counted among the numbers it cannot serve",
912        );
913        assert_eq!(
914            fanout.unringed_between(&seat, 1, 2),
915            1,
916            "and the release beside it is, because no window can hold it",
917        );
918
919        assert!(fanout.send(occupant_change(&seat)), "a subscribed fan-out delivers");
920        for _ in 0..3 {
921            assert!(fanout.send(a_turn_ends(&seat)), "a subscribed fan-out delivers");
922        }
923        assert_eq!(
924            fanout.unringed_between(&seat, 1, 3),
925            0,
926            "the new epoch mints from 1 again, so the old release's number is forgotten",
927        );
928    }
929
930    /// **A release keeps the frames before it reachable.** A seat a connection
931    /// re-opens after its session ended is answered from its record, and that
932    /// read can be behind frames the release ends - the ring is what fills it.
933    /// It is shed at the next epoch's seal, where the numbers start over, not
934    /// here.
935    #[test]
936    fn a_release_keeps_the_frames_before_it_reachable() {
937        let fanout = UpdateFanout::default();
938        let mut rx = fanout.subscribe(SubscriberRole::Answering);
939        let seat = seat("lead");
940
941        assert!(fanout.send(a_turn_ends(&seat)), "a subscribed fan-out delivers");
942        assert!(
943            fanout.send(SessionUpdate::Releasing { key: seat.clone() }),
944            "a subscribed fan-out delivers"
945        );
946        assert_eq!(next_seq(&mut rx, "seat"), Some(1), "the row");
947        assert_eq!(next_seq(&mut rx, "seat"), Some(2), "and the release after it");
948
949        assert_eq!(
950            fanout.frames_between(&seat, 1, 1).iter().map(|frame| frame.seq).collect::<Vec<_>>(),
951            vec![Some(1)],
952            "the frame before the release is still there for a copy that is behind it",
953        );
954        assert_eq!(
955            fanout.unringed_between(&seat, 1, 2),
956            1,
957            "and the release itself is still a number no window holds",
958        );
959    }
960
961    /// **A released seat's ring answers its byte ceiling.** The frames before
962    /// a release are kept so a re-open can fill its copy, but a seat that
963    /// never gets a new occupant must not pin them for the process's life: the
964    /// ceiling drops the oldest, and the newest frame survives even alone over
965    /// it - the same rule the conversation window keeps for the turn it cuts
966    /// inside.
967    #[test]
968    fn a_released_seat_keeps_only_what_its_byte_ceiling_holds() {
969        let fanout = UpdateFanout::default();
970        let mut rx = fanout.subscribe(SubscriberRole::Answering);
971        let seat = seat("lead");
972        // Two of these frames fit; the ceiling is what sheds them, not a
973        // count a fixture had to reach.
974        fanout.set_ring_cap_bytes(10_000);
975
976        assert!(fanout.send(a_big_index(&seat, 20)), "a subscribed fan-out delivers");
977        assert!(
978            fanout.send(SessionUpdate::Releasing { key: seat.clone() }),
979            "a subscribed fan-out delivers"
980        );
981        assert_eq!(next_seq(&mut rx, "seat"), Some(1), "the frame before the release");
982        assert_eq!(next_seq(&mut rx, "seat"), Some(2), "and the release");
983
984        for _ in 0..3 {
985            assert!(fanout.send(a_big_index(&seat, 20)), "a subscribed fan-out delivers");
986        }
987
988        assert!(
989            fanout.ring_bytes(&seat) <= 10_000,
990            "the ring answers its byte ceiling: {} bytes",
991            fanout.ring_bytes(&seat),
992        );
993        assert!(
994            fanout.frames_between(&seat, 1, 1).is_empty(),
995            "and the frame before the release is shed once the ring outgrows it",
996        );
997        assert!(
998            !fanout.frames_between(&seat, 3, 5).is_empty(),
999            "while the newest frames are what a repair is answered from",
1000        );
1001    }
1002
1003    /// **A frame over the ceiling survives alone.** The newest frame is what a
1004    /// repair is answered from, so the cut stops at the last one - the same
1005    /// rule the conversation window keeps for the turn it cuts inside - and
1006    /// the ring states its true weight rather than hiding it.
1007    #[test]
1008    fn a_frame_over_the_ceiling_survives_alone() {
1009        let fanout = UpdateFanout::default();
1010        let mut rx = fanout.subscribe(SubscriberRole::Answering);
1011        let seat = seat("lead");
1012        fanout.set_ring_cap_bytes(10_000);
1013
1014        // About 18 KB, over the ceiling on its own.
1015        assert!(fanout.send(a_big_index(&seat, 60)), "a subscribed fan-out delivers");
1016        assert_eq!(next_seq(&mut rx, "seat"), Some(1), "the frame");
1017        assert_eq!(
1018            fanout.frames_between(&seat, 1, 1).len(),
1019            1,
1020            "the newest frame survives even alone over the ceiling",
1021        );
1022        assert!(
1023            fanout.ring_bytes(&seat) > 10_000,
1024            "and the ring states its true weight: {} bytes",
1025            fanout.ring_bytes(&seat),
1026        );
1027    }
1028
1029    /// **A call output and a card tail are charged by their payload.** Both
1030    /// grow with what a person reads - a command's tail, an instance's calls -
1031    /// and a ceiling that counted them as a frame's fixed weight would let a
1032    /// heavy seat pin its whole window.
1033    #[test]
1034    fn a_call_output_and_a_card_tail_are_charged_by_their_payload() {
1035        use forge_primitives::runtime::{SubagentCall, SubagentCallStatus, SubagentCard};
1036
1037        let fanout = UpdateFanout::default();
1038        let mut rx = fanout.subscribe(SubscriberRole::Answering);
1039        let seat = seat("lead");
1040        let line = "x".repeat(4_000);
1041
1042        assert!(
1043            fanout.send(SessionUpdate::CallOutput {
1044                key: seat.clone(),
1045                call_id: "tu-1".to_owned(),
1046                output: forge_primitives::CallOutput::Lines(vec![
1047                    line.clone(),
1048                    line.clone(),
1049                    line.clone(),
1050                ]),
1051            }),
1052            "a subscribed fan-out delivers",
1053        );
1054        assert_eq!(next_seq(&mut rx, "seat"), Some(1), "the call output");
1055        let output_bytes = fanout.ring_bytes(&seat);
1056        assert!(
1057            output_bytes > 12_000,
1058            "three 4 KB lines are charged against the ceiling: {output_bytes} bytes",
1059        );
1060
1061        let card = SubagentCard {
1062            name: "an instance".to_owned(),
1063            dispatch_id: "tu-2".to_owned(),
1064            agent_type: None,
1065            running: true,
1066            failed: false,
1067            backgrounded: false,
1068            ended_at_ms: None,
1069            calls: 3,
1070            tail: (0..3)
1071                .map(|_| SubagentCall {
1072                    name: "Bash".to_owned(),
1073                    title: line.clone(),
1074                    status: SubagentCallStatus::Completed,
1075                })
1076                .collect(),
1077            usage: None,
1078        };
1079        assert!(
1080            fanout
1081                .send(SessionUpdate::SubagentCardsChanged { key: seat.clone(), cards: vec![card] }),
1082            "a subscribed fan-out delivers",
1083        );
1084        assert_eq!(next_seq(&mut rx, "seat"), Some(2), "and the cards after it");
1085        let both = fanout.ring_bytes(&seat);
1086        assert!(
1087            both > output_bytes + 12_000,
1088            "the card tail's calls are charged too: {both} bytes over the {output_bytes} before \
1089             it",
1090        );
1091    }
1092
1093    /// **A frame is charged by its serialised size, not by a hand-listed set
1094    /// of fields.** `Notice` was not on the old walk, so a notice carrying ten
1095    /// kilobytes was charged a frame's fixed weight - the shape of failure a
1096    /// field added to any frame would repeat. Its own bytes are what it costs
1097    /// now.
1098    #[test]
1099    fn a_frame_is_charged_by_its_serialised_size() {
1100        let fanout = UpdateFanout::default();
1101        let mut rx = fanout.subscribe(SubscriberRole::Answering);
1102        let seat = seat("lead");
1103
1104        assert!(
1105            fanout.send(SessionUpdate::Notice {
1106                key: seat.clone(),
1107                severity: NoticeSeverity::Info,
1108                text: "x".repeat(10_000),
1109            }),
1110            "a subscribed fan-out delivers",
1111        );
1112        assert_eq!(next_seq(&mut rx, "seat"), Some(1), "the notice");
1113        let charged = fanout.ring_bytes(&seat);
1114        assert!(
1115            charged >= 10_000,
1116            "a 10 KB notice is charged its own bytes rather than a frame's weight: {charged}",
1117        );
1118    }
1119
1120    /// A frame that reached nobody still takes its seat's number, so a client
1121    /// joining at the first subscribe has no hole at the join.
1122    #[test]
1123    fn a_frame_that_reached_nobody_still_advances_its_seats_number() {
1124        let fanout = UpdateFanout::default();
1125        let seat = seat("lead");
1126        assert!(!fanout.send(a_turn_ends(&seat)), "nothing has attached to take it");
1127        assert!(!fanout.send(a_turn_ends(&seat)), "nothing has attached to take it");
1128
1129        let mut first = fanout.subscribe(SubscriberRole::Answering);
1130        assert!(fanout.send(a_turn_ends(&seat)), "the subscriber is attached");
1131
1132        assert_eq!(
1133            next_seq(&mut first, "first"),
1134            Some(3),
1135            "the delivered frame continues the count rather than reusing a number",
1136        );
1137    }
1138}