Skip to main content

forge_workspace/
voice.rs

1//! The per-seat voice service: listeners, the sentence queue, and synthesis.
2
3use std::collections::{BTreeSet, HashMap};
4use std::sync::atomic::{AtomicUsize, Ordering};
5use std::sync::{Arc, Mutex};
6
7use forge_primitives::{Message, SessionSlot};
8use forge_tts::{Chunk, Engine, SpeakRequest};
9use tokio::sync::{mpsc, oneshot};
10use tracing::Instrument;
11
12/// What one utterance renders with, from the `[tts]` section.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct VoiceSettings {
15    pub enabled: bool,
16    pub max_utterance_seconds: u64,
17    /// The global voice; absent uses the engine's default.
18    pub voice: Option<String>,
19}
20
21impl Default for VoiceSettings {
22    fn default() -> Self {
23        Self { enabled: true, max_utterance_seconds: 120, voice: None }
24    }
25}
26
27/// The line a voice preview speaks, so two previews can be compared.
28pub const PREVIEW_LINE: &str = "This is how this seat's voice sounds.";
29
30/// Why a `say` did not reach the sink.
31#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
32pub enum SayError {
33    /// No client has voice on for this seat, so there is nowhere to play.
34    #[error("no listener")]
35    NoListener,
36    /// The engine could not load or render; the chat is unaffected.
37    #[error("voice engine failed: {0}")]
38    Engine(String),
39    /// The text is past `[tts] max_utterance_seconds`.
40    #[error("{0}")]
41    TooLong(String),
42    /// The queue is gone, or the render stopped before its first audio.
43    #[error("the voice engine is not ready")]
44    NotReady,
45    /// `[tts] enabled` is false, so nothing may synthesise.
46    #[error("speech is off")]
47    Disabled,
48}
49
50/// Where a rendered clip goes.
51pub trait AudioSink: Send + Sync {
52    fn push(&self, seat: &SessionSlot, chunk: Chunk);
53}
54
55/// How a listener note reaches the seat's session.
56pub trait NoteDispatch: Send + Sync {
57    fn note(&self, seat: &SessionSlot, text: &str);
58}
59
60/// The note the first client's turn-on puts into the seat.
61pub const LISTENING_NOTE: &str = "The user is listening to this seat.\n\nSpeak what they should hear as you would say it in a live conversation: send each message's substance through mcp__forge__voice__say as well as writing it in the chat, keeping the spoken form simpler and shorter so it lands by ear. A reply that is only written is one they will not hear. When the stop note arrives, you may stop speaking.";
62
63/// What the session is told when the last client leaves.
64pub const NOT_LISTENING_NOTE: &str =
65    "The user has stopped listening to this seat.\n\nYou may stop speaking.";
66
67/// The envelope a note rides, so a view draws it as information rather than
68/// as the reader's own turn.
69fn note_envelope(text: &str) -> String {
70    format!("[Voice]\n\n{text}")
71}
72
73type Loader = Arc<dyn Fn() -> Result<Arc<dyn Engine>, String> + Send + Sync>;
74
75/// The per-seat voice service. One render queue per seat, so a second `say`
76/// waits behind the first, and one engine for the whole process, loaded at
77/// boot and kept for the process's life; a failed warm is retried by the
78/// first `say`.
79#[derive(Clone)]
80pub struct VoiceService {
81    loader: Loader,
82    engine: Arc<tokio::sync::Mutex<Option<Arc<dyn Engine>>>>,
83    settings: VoiceSettings,
84    sink: Arc<dyn AudioSink>,
85    notes: Arc<dyn NoteDispatch>,
86    seats: Arc<Mutex<HashMap<SessionSlot, Seat>>>,
87    /// The previews one seat may have in flight: a held key updates the
88    /// newest voice instead of queueing one render per press.
89    previews: Arc<Mutex<HashMap<SessionSlot, PreviewSlot>>>,
90}
91
92#[derive(Default)]
93struct PreviewSlot {
94    latest: Option<String>,
95    running: bool,
96    /// The loop that owns `running`: a settle from an older loop must not
97    /// clear a newer loop's flag or drop the voice it has yet to render.
98    generation: u64,
99    /// Bumped on every accepted press, so a chunk from a render the reader has
100    /// already moved past is dropped rather than played.
101    presses: u64,
102}
103
104struct Seat {
105    listeners: BTreeSet<u64>,
106    queue: mpsc::UnboundedSender<Job>,
107    /// The listener count the render task reads, so it stops between
108    /// sentences the moment the last one leaves rather than draining a
109    /// queue nobody will hear.
110    live: Arc<AtomicUsize>,
111    /// The session has been told the user is listening and not yet told they
112    /// stopped. A pause keeps it true, so a seat switch does not toggle the
113    /// agent; an explicit off clears it.
114    noted: bool,
115}
116
117struct Job {
118    text: String,
119    voice: Option<String>,
120    engine: Arc<dyn Engine>,
121    first: oneshot::Sender<Result<(), SayError>>,
122    /// The seat's press count when this preview was taken up. A chunk from an
123    /// older press is dropped rather than played: the reader has moved on.
124    preview: Option<u64>,
125}
126
127impl VoiceService {
128    pub fn new(
129        loader: Loader,
130        settings: VoiceSettings,
131        sink: Arc<dyn AudioSink>,
132        notes: Arc<dyn NoteDispatch>,
133    ) -> Self {
134        Self {
135            loader,
136            engine: Arc::new(tokio::sync::Mutex::new(None)),
137            settings,
138            sink,
139            notes,
140            seats: Arc::new(Mutex::new(HashMap::new())),
141            previews: Arc::new(Mutex::new(HashMap::new())),
142        }
143    }
144
145    /// Queue one utterance for `seat`. Answers once the first audio is
146    /// rendered, so a named engine failure reaches the caller rather than
147    /// disappearing into a playback queue.
148    pub async fn say(&self, seat: &SessionSlot, text: String) -> Result<(), SayError> {
149        if !self.settings.enabled {
150            return Err(SayError::Disabled);
151        }
152        // Refuse before the load, so a seat nobody hears never pays it.
153        self.listening_queue(seat)?;
154        forge_tts::within_bound(&text, self.settings.max_utterance_seconds)
155            .map_err(|error| SayError::TooLong(error.to_string()))?;
156
157        let engine = self.engine().await?;
158        // The listener can leave while the engine loads; ask again so the
159        // say answers the empty seat rather than blaming the engine.
160        let queue = self.listening_queue(seat)?;
161        let (first, outcome) = oneshot::channel();
162        let voice = self.settings.voice.clone();
163        queue
164            .send(Job { text, voice, engine, first, preview: None })
165            .map_err(|_| SayError::NotReady)?;
166        outcome.await.map_err(|_| SayError::NotReady)?
167    }
168
169    /// Render one fixed line in `voice` on `seat`'s own stream, so a voice is
170    /// heard before it is chosen. The seat's speaker switch is the playback
171    /// gate: nobody listening means nowhere to play.
172    ///
173    /// **One render loop per seat.** While a preview renders, a second press
174    /// replaces the pending voice instead of queueing another render, so the
175    /// newest press is the one heard and a held key costs one render.
176    pub async fn preview(&self, seat: &SessionSlot, voice: String) -> Result<(), SayError> {
177        if !self.settings.enabled {
178            return Err(SayError::Disabled);
179        }
180        // Refuse before the load, so a seat nobody hears never pays it.
181        self.listening_queue(seat)?;
182        let mut generation = {
183            let mut previews = self.previews.lock().map_err(|_| SayError::NotReady)?;
184            let slot = previews.entry(seat.clone()).or_default();
185            slot.latest = Some(voice);
186            slot.presses += 1;
187            if slot.running {
188                return Ok(());
189            }
190            slot.running = true;
191            slot.generation += 1;
192            slot.generation
193        };
194        loop {
195            let outcome = self.render_previews(seat).await;
196            // The loop ends however it ends, and a press that landed after
197            // its last take is still waiting: it is rendered here rather
198            // than dropped by the settle.
199            let Some(pending) = self.settle_previews(seat, generation) else {
200                return outcome;
201            };
202            let Some(next) = self.restart_previews(seat, pending) else {
203                return outcome;
204            };
205            generation = next;
206        }
207    }
208
209    /// End the loop `generation` owns and hand back the voice a later press
210    /// left waiting. A newer loop's flag and voice are its own, and neither
211    /// is touched.
212    fn settle_previews(&self, seat: &SessionSlot, generation: u64) -> Option<String> {
213        let mut previews = self.previews.lock().ok()?;
214        let slot = previews.get_mut(seat)?;
215        if slot.generation != generation {
216            return None;
217        }
218        slot.running = false;
219        slot.latest.take()
220    }
221
222    /// Claim the seat for a waiting voice. `None` when a newer loop already
223    /// holds it: that loop carries the newest press, so an older voice is the
224    /// one to drop.
225    fn restart_previews(&self, seat: &SessionSlot, voice: String) -> Option<u64> {
226        let mut previews = self.previews.lock().ok()?;
227        let slot = previews.get_mut(seat)?;
228        if slot.running {
229            return None;
230        }
231        slot.latest = Some(voice);
232        slot.running = true;
233        slot.generation += 1;
234        Some(slot.generation)
235    }
236
237    /// Drain the seat's pending previews, newest last.
238    async fn render_previews(&self, seat: &SessionSlot) -> Result<(), SayError> {
239        loop {
240            // The take and the running flag move under one lock, so a press
241            // that lands as this loop exits either starts its own loop or is
242            // the voice this one renders.
243            let next = {
244                let mut previews = self.previews.lock().map_err(|_| SayError::NotReady)?;
245                let Some(slot) = previews.get_mut(seat) else {
246                    return Ok(());
247                };
248                let next = slot.latest.take().map(|voice| (voice, slot.presses));
249                if next.is_none() {
250                    slot.running = false;
251                }
252                next
253            };
254            let Some((voice, presses)) = next else {
255                return Ok(());
256            };
257            let engine = self.engine().await?;
258            // The listener can leave while the engine loads; a preview with
259            // nowhere to play is done rather than an error.
260            let Ok(queue) = self.listening_queue(seat) else {
261                return Ok(());
262            };
263            let (first, outcome) = oneshot::channel();
264            queue
265                .send(Job {
266                    text: PREVIEW_LINE.to_owned(),
267                    voice: Some(voice),
268                    engine,
269                    first,
270                    preview: Some(presses),
271                })
272                .map_err(|_| SayError::NotReady)?;
273            outcome.await.map_err(|_| SayError::NotReady)??;
274        }
275    }
276
277    /// The queue of a seat that still has a listener. A `say` reads it again
278    /// after a load, because the seat can empty while the engine loads.
279    fn listening_queue(&self, seat: &SessionSlot) -> Result<mpsc::UnboundedSender<Job>, SayError> {
280        let seats = self.seats.lock().map_err(|_| SayError::NotReady)?;
281        match seats.get(seat) {
282            Some(state) if !state.listeners.is_empty() => Ok(state.queue.clone()),
283            _ => Err(SayError::NoListener),
284        }
285    }
286
287    /// Turn one client's voice on or off for `seat`. The session is told on
288    /// the first client in, and on the last client out only when the reader
289    /// turned it off themselves: a move to another seat pauses the seat
290    /// without toggling the agent.
291    pub fn set_listening(&self, seat: &SessionSlot, client: u64, on: bool, explicit: bool) {
292        let note = {
293            let Ok(mut seats) = self.seats.lock() else {
294                return;
295            };
296            if on {
297                if let Some(state) = seats.get_mut(seat) {
298                    let first = state.listeners.is_empty();
299                    state.listeners.insert(client);
300                    if first {
301                        // The previous queue task stopped when the last
302                        // listener left; a return gets a fresh one.
303                        let live = Arc::new(AtomicUsize::new(1));
304                        state.queue = self.spawn_queue(seat, Arc::clone(&live));
305                        state.live = live;
306                    } else {
307                        state.live.fetch_add(1, Ordering::SeqCst);
308                    }
309                    if first && !state.noted {
310                        state.noted = true;
311                        Some(LISTENING_NOTE)
312                    } else {
313                        None
314                    }
315                } else {
316                    let live = Arc::new(AtomicUsize::new(1));
317                    let queue = self.spawn_queue(seat, Arc::clone(&live));
318                    seats.insert(
319                        seat.clone(),
320                        Seat { listeners: BTreeSet::from([client]), queue, live, noted: true },
321                    );
322                    Some(LISTENING_NOTE)
323                }
324            } else {
325                let Some(state) = seats.get_mut(seat) else {
326                    return;
327                };
328                if !state.listeners.remove(&client) {
329                    return;
330                }
331                if state.listeners.is_empty() {
332                    state.live.store(0, Ordering::SeqCst);
333                    if explicit {
334                        state.noted = false;
335                        seats.remove(seat);
336                        Some(NOT_LISTENING_NOTE)
337                    } else {
338                        // A pause: the seat keeps its note, so a return does
339                        // not tell the agent it came back.
340                        None
341                    }
342                } else {
343                    state.live.fetch_sub(1, Ordering::SeqCst);
344                    None
345                }
346            }
347        };
348        if let Some(text) = note {
349            self.notes.note(seat, text);
350        }
351    }
352
353    /// Swap the loaded engine for one already built, so a model change keeps
354    /// serving until the new engine is up.
355    pub async fn replace_engine(&self, engine: Arc<dyn Engine>) {
356        *self.engine.lock().await = Some(engine);
357    }
358
359    pub fn listeners(&self, seat: &SessionSlot) -> usize {
360        self.seats
361            .lock()
362            .ok()
363            .and_then(|seats| seats.get(seat).map(|state| state.listeners.len()))
364            .unwrap_or(0)
365    }
366
367    /// A connection has gone: take it off every seat it was listening to,
368    /// and answer the seats whose count changed. A socket that dies says no
369    /// goodbye, so this teardown is what lowers the count.
370    pub fn release(&self, client: u64) -> Vec<SessionSlot> {
371        let (changed, emptied) = {
372            let Ok(mut seats) = self.seats.lock() else {
373                return Vec::new();
374            };
375            let mut changed = Vec::new();
376            let mut emptied = Vec::new();
377            for (seat, state) in seats.iter_mut() {
378                if !state.listeners.remove(&client) {
379                    continue;
380                }
381                changed.push(seat.clone());
382                if state.listeners.is_empty() {
383                    state.live.store(0, Ordering::SeqCst);
384                    emptied.push(seat.clone());
385                } else {
386                    state.live.fetch_sub(1, Ordering::SeqCst);
387                }
388            }
389            for seat in &emptied {
390                seats.remove(seat);
391            }
392            (changed, emptied)
393        };
394        for seat in &emptied {
395            self.notes.note(seat, NOT_LISTENING_NOTE);
396        }
397        changed
398    }
399
400    /// Whether `[tts] enabled` is set. A disabled service registers no tool
401    /// and refuses every `say`.
402    pub fn enabled(&self) -> bool {
403        self.settings.enabled
404    }
405
406    /// The seat's occupant changed under a listening client: tell the new
407    /// session it is heard, because the note fires on the listener set's
408    /// transitions and a `/new` or `/resume` leaves that set alone.
409    pub fn occupant_changed(&self, seat: &SessionSlot) {
410        let listening = self
411            .seats
412            .lock()
413            .ok()
414            .and_then(|seats| seats.get(seat).map(|state| !state.listeners.is_empty()))
415            .unwrap_or(false);
416        if listening {
417            self.notes.note(seat, LISTENING_NOTE);
418        }
419    }
420
421    /// Load the engine ahead of the first `say`. A failed load leaves the
422    /// slot empty, so the first `say` retries it.
423    pub(crate) async fn warm(&self) -> Result<(), SayError> {
424        self.engine().await.map(|_held| ())
425    }
426
427    /// The engine, loading it on the first caller. The mutex is held across
428    /// the load, so a second `say` waits for it instead of racing it.
429    async fn engine(&self) -> Result<Arc<dyn Engine>, SayError> {
430        let mut slot = self.engine.lock().await;
431        if let Some(engine) = slot.as_ref() {
432            return Ok(Arc::clone(engine));
433        }
434        let loader = Arc::clone(&self.loader);
435        let result = tokio::task::spawn_blocking(move || loader())
436            .await
437            .map_err(|error| SayError::Engine(error.to_string()))?;
438        let engine = result.map_err(SayError::Engine)?;
439        *slot = Some(Arc::clone(&engine));
440        Ok(engine)
441    }
442
443    /// The seat's render loop. It stops the moment the last listener leaves,
444    /// so a queued utterance is not rendered into a seat nobody hears.
445    fn spawn_queue(
446        &self,
447        seat: &SessionSlot,
448        live: Arc<AtomicUsize>,
449    ) -> mpsc::UnboundedSender<Job> {
450        let (queue, mut jobs) = mpsc::unbounded_channel::<Job>();
451        let sink = Arc::clone(&self.sink);
452        let previews = Arc::clone(&self.previews);
453        let seat = seat.clone();
454        tokio::spawn(async move {
455            let mut seq = 0_u32;
456            while live.load(Ordering::SeqCst) > 0 {
457                let Some(job) = jobs.recv().await else {
458                    break;
459                };
460                if live.load(Ordering::SeqCst) == 0 {
461                    break;
462                }
463                seq = render(job, &seat, sink.as_ref(), &live, seq, previews.as_ref()).await;
464            }
465        });
466        queue
467    }
468}
469
470impl crate::Workspace {
471    /// The voice service, built on first use - the boot prewarm is the first
472    /// caller when `[tts]` is on. Its sink and note dispatch hold a
473    /// `Weak<Workspace>`, which is why this waits for an `Arc<Self>` rather
474    /// than being built with the rest of the value.
475    pub(crate) fn voice_service(self: &Arc<Self>) -> Arc<VoiceService> {
476        self.voice
477            .get_or_init(|| {
478                let weak = Arc::downgrade(self);
479                let loader: Loader = Arc::new(move || {
480                    let workspace =
481                        weak.upgrade().ok_or_else(|| "the workspace is gone".to_owned())?;
482                    workspace.load_speaking_engine()
483                });
484                Arc::new(self.build_voice_service(loader))
485            })
486            .clone()
487    }
488
489    /// Load the speaking engine for the active variant.
490    ///
491    /// The configured voice is checked against the installed set before the
492    /// model loads, so a bad id is refused by name at warm rather than
493    /// silently replaced by the engine's own single-voice fallback.
494    fn load_speaking_engine(&self) -> Result<Arc<dyn Engine>, String> {
495        let files = self.speaking_files().ok_or_else(|| {
496            "the speaking model is not installed; install it from the models page".to_owned()
497        })?;
498        let installed = files.installed_voices().map_err(|error| error.to_string())?;
499        forge_tts::select_voice(self.config_arc().tts.voice.as_deref(), &installed)
500            .map_err(|error| error.to_string())?;
501        let engine = forge_tts::KokoroTts::load(&files).map_err(|error| error.to_string())?;
502        Ok(Arc::new(engine) as Arc<dyn Engine>)
503    }
504
505    /// Load the speaking engine beside boot, so the first `say` pays no
506    /// model load. One task per forge run, spawned rather than awaited; a
507    /// failed load leaves the slot empty, which the first `say` fills.
508    ///
509    /// The slot is only ever filled, never emptied: a loaded engine is never
510    /// taken away, and a `say` that arrives while this load is still running
511    /// waits on the same lock rather than answering [`SayError::NotReady`].
512    pub fn start_voice_prewarm(self: &Arc<Self>) {
513        // The wave-asked signal, kept before the gate: it is what a test
514        // reads on a fixture with `[tts]` off. The dispatch has its own
515        // guard, which only stops a second load of the same engine.
516        self.voice_prewarm_started.store(true, Ordering::Release);
517        let settings = self.config_arc().tts.clone();
518        if !settings.enabled {
519            return;
520        }
521        if self.voice_prewarm_dispatched.swap(true, Ordering::AcqRel) {
522            return;
523        }
524        let files = self.speaking_files();
525        let model = files
526            .as_ref()
527            .map_or_else(|| "unresolved".to_owned(), |files| files.model.display().to_string());
528        let dir = settings
529            .models_dir()
530            .map_or_else(|| "unresolved".to_owned(), |dir| dir.display().to_string());
531        let service = self.voice_service();
532        let started = std::time::Instant::now();
533        let span = tracing::info_span!("voice_prewarm");
534        tracing::info!(
535            event_name = "voice_prewarm_starting",
536            models_dir = %dir,
537            model = %model,
538            "loading the speaking engine"
539        );
540        tokio::spawn(
541            async move {
542                match service.warm().await {
543                    Ok(()) => tracing::info!(
544                        event_name = "voice_prewarm_ready",
545                        elapsed_ms = started.elapsed().as_millis(),
546                        model = %model,
547                        "the speaking engine is resident; a say pays no load"
548                    ),
549                    Err(error) => tracing::warn!(
550                        event_name = "voice_prewarm_failed",
551                        model = %model,
552                        failure = %error,
553                        "the speaking engine did not load; the first say will retry"
554                    ),
555                }
556            }
557            .instrument(span),
558        );
559    }
560
561    /// Whether speech is on, as the running system answers it: the built
562    /// service's own flag when it exists, else the config that would build
563    /// one. A save marked restart cannot move one side without the other.
564    pub(crate) fn voice_enabled(&self) -> bool {
565        self.voice.get().map_or_else(|| self.config_arc().tts.enabled, |service| service.enabled())
566    }
567
568    /// The clients with voice on for `seat`. Zero before the service has
569    /// been built, which is the truth: nobody could have toggled it yet.
570    pub fn voice_listeners(&self, seat: &SessionSlot) -> usize {
571        self.voice.get().map_or(0, |service| service.listeners(seat))
572    }
573
574    /// The seat's occupant changed under a listening client: tell the new
575    /// session it is heard. A no-op before the service exists, because
576    /// nobody could have toggled voice on.
577    pub fn voice_occupant_changed(&self, seat: &SessionSlot) {
578        if let Some(service) = self.voice.get() {
579            service.occupant_changed(seat);
580        }
581    }
582
583    /// A connection has left: take it off every seat it was listening to and
584    /// tell each seat's subscribers the count it left behind.
585    pub fn voice_release(&self, client: u64) -> Vec<SessionSlot> {
586        let Some(service) = self.voice.get() else {
587            return Vec::new();
588        };
589        let changed = service.release(client);
590        for seat in &changed {
591            let listeners = u32::try_from(service.listeners(seat)).unwrap_or(u32::MAX);
592            let _ = self
593                .update_sender()
594                .send(crate::SessionUpdate::VoiceModeState { key: seat.clone(), listeners });
595        }
596        changed
597    }
598
599    /// Put an already-built engine behind the service, so a test can drive a
600    /// render without loading a model.
601    #[cfg(any(test, feature = "test-helpers"))]
602    pub fn install_voice_engine_for_test(self: &Arc<Self>, engine: Arc<dyn Engine>) {
603        let loader: Loader = Arc::new(move || Ok(Arc::clone(&engine)));
604        let _ = self.voice.set(Arc::new(self.build_voice_service(loader)));
605    }
606
607    /// Speak one utterance the way `voice__say` does, so a cross-crate test
608    /// can drive a render without an agent calling the tool.
609    #[cfg(any(test, feature = "test-helpers"))]
610    pub async fn voice_say_for_test(
611        self: &Arc<Self>,
612        seat: &SessionSlot,
613        text: String,
614    ) -> Result<(), SayError> {
615        self.voice_service().say(seat, text).await
616    }
617
618    fn build_voice_service(self: &Arc<Self>, loader: Loader) -> VoiceService {
619        let config = self.config_arc();
620        let settings = VoiceSettings {
621            enabled: config.tts.enabled,
622            max_utterance_seconds: config.tts.max_utterance_seconds,
623            voice: config.tts.voice.clone(),
624        };
625        VoiceService::new(
626            loader,
627            settings,
628            Arc::new(WorkspaceSink { workspace: Arc::downgrade(self) }),
629            Arc::new(WorkspaceNotes { workspace: Arc::downgrade(self) }),
630        )
631    }
632}
633
634/// The sink a real workspace hands the service: every rendered clip crosses
635/// the update stream to the clients subscribed to the seat.
636struct WorkspaceSink {
637    workspace: std::sync::Weak<crate::Workspace>,
638}
639
640impl AudioSink for WorkspaceSink {
641    fn push(&self, seat: &SessionSlot, chunk: Chunk) {
642        let Some(workspace) = self.workspace.upgrade() else {
643            return;
644        };
645        // A seat that has ended has no one to speak to, even if a stale
646        // listener remains.
647        if workspace.domain_session_for(seat).is_none() {
648            return;
649        }
650        let _ = workspace.update_sender().send(crate::SessionUpdate::VoiceAudio {
651            key: seat.clone(),
652            seq: chunk.seq,
653            sample_rate: chunk.sample_rate,
654            pcm: chunk.pcm,
655        });
656    }
657}
658
659/// The note dispatch a real workspace hands the service: the listener note
660/// enters the seat as a forge prompt, drawn as its own row.
661struct WorkspaceNotes {
662    workspace: std::sync::Weak<crate::Workspace>,
663}
664
665impl NoteDispatch for WorkspaceNotes {
666    fn note(&self, seat: &SessionSlot, text: &str) {
667        let Some(workspace) = self.workspace.upgrade() else {
668            return;
669        };
670        let uuid = forge_sdk::request_id::next_prompt_id();
671        let prose = note_envelope(text);
672        if let Err(error) = workspace.dispatch_workspace_prompt_under(
673            seat,
674            prose.clone(),
675            crate::PromptSource::Forge,
676            uuid.clone(),
677        ) {
678            tracing::warn!(
679                event_name = "voice_note_not_delivered",
680                seat = %seat.display(),
681                %error,
682                "the listener note did not reach the seat; the agent will not know it is heard"
683            );
684            return;
685        }
686        // The CLI does not echo a prompt, so this display row is what the
687        // reader's page draws: the same forged turn a delivery carries.
688        let mut row = Message::display_only_user(prose, uuid);
689        row.stamp_if_absent(&crate::install::rfc3339_now());
690        let _ = workspace.update_sender().send(crate::SessionUpdate::ChatAppended {
691            key: seat.clone(),
692            msg: row,
693            source: Some(crate::PromptSource::Forge),
694        });
695    }
696}
697
698async fn render(
699    job: Job,
700    seat: &SessionSlot,
701    sink: &dyn AudioSink,
702    live: &AtomicUsize,
703    mut seq: u32,
704    previews: &Mutex<HashMap<SessionSlot, PreviewSlot>>,
705) -> u32 {
706    let Job { text, voice, engine, first, preview } = job;
707    let mut first = Some(first);
708    for sentence in forge_tts::sentences(&text) {
709        if live.load(Ordering::SeqCst) == 0 {
710            return seq;
711        }
712        let mut chunks = engine.speak(SpeakRequest { text: sentence, voice: voice.clone() });
713        while let Some(result) = chunks.recv().await {
714            if live.load(Ordering::SeqCst) == 0 {
715                return seq;
716            }
717            match result {
718                Ok(chunk) => {
719                    // A preview the reader has already pressed past is not
720                    // played: the newest press is the one heard, and this
721                    // chunk is the abandoned voice still finishing.
722                    let stale = preview.is_some_and(|presses| {
723                        previews.lock().map_or(true, |map| {
724                            map.get(seat).is_none_or(|slot| slot.presses != presses)
725                        })
726                    });
727                    if let Some(sender) = first.take() {
728                        let _ = sender.send(Ok(()));
729                    }
730                    if !stale {
731                        sink.push(seat, Chunk { seq, ..chunk });
732                    }
733                    seq = seq.wrapping_add(1);
734                }
735                Err(error) => {
736                    tracing::warn!(
737                        event_name = "voice_render_failed",
738                        slot = %seat.display(),
739                        %error,
740                        "a sentence did not render; the rest of the utterance is dropped"
741                    );
742                    if let Some(sender) = first.take() {
743                        let _ = sender.send(Err(SayError::Engine(error.to_string())));
744                    }
745                    return seq;
746                }
747            }
748        }
749    }
750    if let Some(sender) = first {
751        let _ = sender.send(Ok(()));
752    }
753    seq
754}
755
756#[cfg(test)]
757mod tests {
758    use std::sync::atomic::{AtomicUsize, Ordering};
759    use std::sync::{Arc, Mutex};
760    use std::time::Duration;
761
762    use forge_primitives::SessionSlot;
763    use forge_tts::{Chunk, Engine, Error, SpeakRequest};
764    use tokio::sync::mpsc;
765
766    use super::{
767        AudioSink, LISTENING_NOTE, NOT_LISTENING_NOTE, NoteDispatch, SayError, VoiceService,
768        VoiceSettings, note_envelope,
769    };
770
771    fn seat() -> SessionSlot {
772        SessionSlot::new("Org", "project", "lead")
773    }
774
775    struct RecordingSink {
776        chunks: Mutex<Vec<(SessionSlot, Chunk)>>,
777    }
778
779    impl AudioSink for RecordingSink {
780        fn push(&self, seat: &SessionSlot, chunk: Chunk) {
781            self.chunks.lock().expect("unpoisoned").push((seat.clone(), chunk));
782        }
783    }
784
785    struct RecordingNotes {
786        said: Mutex<Vec<String>>,
787    }
788
789    impl NoteDispatch for RecordingNotes {
790        fn note(&self, _seat: &SessionSlot, text: &str) {
791            self.said.lock().expect("unpoisoned").push(text.to_owned());
792        }
793    }
794
795    /// Records each request and answers one chunk, so a sink's order is the
796    /// engine's order. The voice each request named is recorded too, so a test
797    /// can say which press was rendered.
798    struct StubEngine {
799        requests: Arc<Mutex<Vec<String>>>,
800        voices: Arc<Mutex<Vec<Option<String>>>>,
801        gate: Option<Arc<tokio::sync::Semaphore>>,
802    }
803
804    impl StubEngine {
805        fn new(requests: Arc<Mutex<Vec<String>>>) -> Self {
806            Self { requests, voices: Arc::new(Mutex::new(Vec::new())), gate: None }
807        }
808    }
809
810    impl Engine for StubEngine {
811        fn speak(&self, request: SpeakRequest) -> mpsc::Receiver<Result<Chunk, Error>> {
812            self.requests.lock().expect("unpoisoned").push(request.text);
813            self.voices.lock().expect("unpoisoned").push(request.voice);
814            let (sender, receiver) = mpsc::channel(2);
815            let permit = self.gate.as_ref().map(|gate| gate.clone().acquire_owned());
816            tokio::spawn(async move {
817                if let Some(permit) = permit {
818                    drop(permit.await);
819                }
820                let _ = sender
821                    .send(Ok(Chunk { seq: 0, sample_rate: 24_000, pcm: vec![1, 2, 3] }))
822                    .await;
823            });
824            receiver
825        }
826    }
827
828    struct Fixture {
829        service: VoiceService,
830        sink: Arc<RecordingSink>,
831        notes: Arc<RecordingNotes>,
832        requests: Arc<Mutex<Vec<String>>>,
833        loads: Arc<AtomicUsize>,
834    }
835
836    fn service_with(engine: impl Fn() -> StubEngine + Send + Sync + 'static) -> Fixture {
837        let requests = Arc::new(Mutex::new(Vec::new()));
838        let sink = Arc::new(RecordingSink { chunks: Mutex::new(Vec::new()) });
839        let notes = Arc::new(RecordingNotes { said: Mutex::new(Vec::new()) });
840        let loads = Arc::new(AtomicUsize::new(0));
841        let factory = Arc::new(engine);
842        let loader = {
843            let factory = Arc::clone(&factory);
844            let requests = Arc::clone(&requests);
845            let loads = Arc::clone(&loads);
846            move || {
847                loads.fetch_add(1, Ordering::SeqCst);
848                let mut engine = factory();
849                engine.requests = Arc::clone(&requests);
850                Ok(Arc::new(engine) as Arc<dyn Engine>)
851            }
852        };
853        Fixture {
854            service: VoiceService::new(
855                Arc::new(loader),
856                VoiceSettings::default(),
857                Arc::clone(&sink) as Arc<dyn AudioSink>,
858                Arc::clone(&notes) as Arc<dyn NoteDispatch>,
859            ),
860            sink,
861            notes,
862            requests,
863            loads,
864        }
865    }
866
867    async fn wait_for(chunks: &Arc<RecordingSink>, expected: usize) {
868        for _ in 0..200 {
869            if chunks.chunks.lock().expect("unpoisoned").len() >= expected {
870                return;
871            }
872            tokio::time::sleep(Duration::from_millis(5)).await;
873        }
874        panic!("the sink saw fewer than {expected} chunks");
875    }
876
877    #[tokio::test]
878    async fn a_say_with_no_listener_is_refused_without_touching_the_engine() {
879        let fixture = service_with(|| StubEngine::new(Arc::new(Mutex::new(Vec::new()))));
880        let outcome = fixture.service.say(&seat(), "Hello.".to_owned()).await;
881        assert_eq!(outcome, Err(SayError::NoListener));
882        assert!(fixture.requests.lock().expect("unpoisoned").is_empty(), "the engine is untouched");
883        assert_eq!(fixture.loads.load(Ordering::SeqCst), 0, "the 2.3 GB model is never loaded");
884    }
885
886    /// A preview is playback on a seat's own stream, so no listener is a
887    /// refusal before the engine is touched.
888    #[tokio::test]
889    async fn a_preview_with_no_listener_is_refused_without_touching_the_engine() {
890        let fixture = service_with(|| StubEngine::new(Arc::new(Mutex::new(Vec::new()))));
891        let outcome = fixture.service.preview(&seat(), "af_heart".to_owned()).await;
892        assert_eq!(outcome, Err(SayError::NoListener));
893        assert!(fixture.requests.lock().expect("unpoisoned").is_empty());
894        assert_eq!(fixture.loads.load(Ordering::SeqCst), 0);
895    }
896
897    /// **One preview loop per seat.** A render in flight swallows later
898    /// presses into its pending voice, so a held key cannot queue a render
899    /// per press; the newest press is the one heard after the one running.
900    #[tokio::test]
901    async fn previews_coalesce_to_the_newest_voice_per_seat() {
902        struct Gated {
903            voices: Arc<Mutex<Vec<String>>>,
904            permits: Arc<tokio::sync::Semaphore>,
905        }
906
907        impl Engine for Gated {
908            fn speak(&self, request: SpeakRequest) -> mpsc::Receiver<Result<Chunk, Error>> {
909                self.voices.lock().expect("unpoisoned").push(request.voice.unwrap_or_default());
910                let (sender, receiver) = mpsc::channel(2);
911                let permits = Arc::clone(&self.permits);
912                tokio::spawn(async move {
913                    let permit = permits.acquire_owned().await;
914                    drop(permit);
915                    let _ =
916                        sender.send(Ok(Chunk { seq: 0, sample_rate: 24_000, pcm: vec![1] })).await;
917                });
918                receiver
919            }
920        }
921
922        let voices = Arc::new(Mutex::new(Vec::new()));
923        let permits = Arc::new(tokio::sync::Semaphore::new(0));
924        let loader: super::Loader = {
925            let voices = Arc::clone(&voices);
926            let permits = Arc::clone(&permits);
927            Arc::new(move || {
928                Ok(Arc::new(Gated { voices: Arc::clone(&voices), permits: Arc::clone(&permits) })
929                    as Arc<dyn Engine>)
930            })
931        };
932        let service = VoiceService::new(
933            loader,
934            VoiceSettings::default(),
935            Arc::new(RecordingSink { chunks: Mutex::new(Vec::new()) }) as Arc<dyn AudioSink>,
936            Arc::new(RecordingNotes { said: Mutex::new(Vec::new()) }) as Arc<dyn NoteDispatch>,
937        );
938        let seat = seat();
939        service.set_listening(&seat, 1, true, false);
940
941        let running = tokio::spawn({
942            let service = service.clone();
943            let seat = seat.clone();
944            async move { service.preview(&seat, "af_heart".to_owned()).await }
945        });
946        for _ in 0..200 {
947            if !voices.lock().expect("unpoisoned").is_empty() {
948                break;
949            }
950            tokio::time::sleep(Duration::from_millis(5)).await;
951        }
952
953        // Both land while the first render is blocked; only the last is owed
954        // a render of its own.
955        service.preview(&seat, "bf_emma".to_owned()).await.expect("queued");
956        service.preview(&seat, "am_adam".to_owned()).await.expect("queued");
957
958        permits.add_permits(2);
959        running.await.expect("the task joins").expect("the first preview renders");
960        for _ in 0..200 {
961            if voices.lock().expect("unpoisoned").len() >= 2 {
962                break;
963            }
964            tokio::time::sleep(Duration::from_millis(5)).await;
965        }
966
967        assert_eq!(
968            voices.lock().expect("unpoisoned").clone(),
969            vec!["af_heart".to_owned(), "am_adam".to_owned()],
970            "the running voice, then the newest press - never the one it replaced",
971        );
972    }
973
974    /// **The listener leaves while the engine loads.** The preview loop is
975    /// already running when the load finishes with nowhere to play; the seat
976    /// must come back to rest, so the next press renders instead of finding a
977    /// loop that claims to be running and never runs.
978    #[tokio::test]
979    async fn a_listener_that_leaves_while_the_engine_loads_does_not_wedge_previews() {
980        let requests = Arc::new(Mutex::new(Vec::new()));
981        let (started, began) = std::sync::mpsc::channel::<()>();
982        let (release, released) = std::sync::mpsc::channel::<()>();
983        let released = Arc::new(Mutex::new(released));
984        let loader: super::Loader = {
985            let requests = Arc::clone(&requests);
986            Arc::new(move || {
987                let _ = started.send(());
988                released.lock().expect("unpoisoned").recv().ok();
989                Ok(Arc::new(StubEngine::new(Arc::clone(&requests))) as Arc<dyn Engine>)
990            })
991        };
992        let service = VoiceService::new(
993            loader,
994            VoiceSettings::default(),
995            Arc::new(RecordingSink { chunks: Mutex::new(Vec::new()) }) as Arc<dyn AudioSink>,
996            Arc::new(RecordingNotes { said: Mutex::new(Vec::new()) }) as Arc<dyn NoteDispatch>,
997        );
998        let seat = seat();
999        service.set_listening(&seat, 1, true, false);
1000
1001        let preview = tokio::spawn({
1002            let service = service.clone();
1003            let seat = seat.clone();
1004            async move { service.preview(&seat, "af_heart".to_owned()).await }
1005        });
1006        let mut loader_started = false;
1007        for _ in 0..200 {
1008            if began.try_recv().is_ok() {
1009                loader_started = true;
1010                break;
1011            }
1012            tokio::time::sleep(Duration::from_millis(5)).await;
1013        }
1014        assert!(loader_started, "the loader starts");
1015        // The listener leaves while the load is still blocked; a pause keeps
1016        // the seat, so a return needs no new note.
1017        service.set_listening(&seat, 1, false, false);
1018        release.send(()).expect("the loader is released");
1019
1020        preview.await.expect("the task joins").expect("the preview ends without an error");
1021
1022        // A listener returns and the next press renders: the seat was not left
1023        // claiming a preview runs.
1024        service.set_listening(&seat, 1, true, false);
1025        service.preview(&seat, "bf_emma".to_owned()).await.expect("the preview renders");
1026        assert_eq!(requests.lock().expect("unpoisoned").len(), 1);
1027    }
1028
1029    /// A press lands after a loop's last take but before that loop settles:
1030    /// the old settle must leave the newer loop's flag and pending voice
1031    /// alone, or the press is dropped and a third one starts a second loop.
1032    #[tokio::test]
1033    async fn a_stale_loop_does_not_settle_a_newer_press() {
1034        let service = service_with(|| StubEngine::new(Arc::new(Mutex::new(Vec::new())))).service;
1035        let seat = seat();
1036        {
1037            let mut previews = service.previews.lock().expect("unpoisoned");
1038            let slot = previews.entry(seat.clone()).or_default();
1039            slot.generation = 2;
1040            slot.running = true;
1041            slot.latest = Some("bf_emma".to_owned());
1042        }
1043
1044        assert_eq!(service.settle_previews(&seat, 1), None, "a stale loop settles nothing");
1045
1046        let previews = service.previews.lock().expect("unpoisoned");
1047        let slot = previews.get(&seat).expect("the slot");
1048        assert!(slot.running, "a stale loop's settle cleared the newer loop's flag");
1049        assert_eq!(slot.latest.as_deref(), Some("bf_emma"), "the newer press's voice was dropped");
1050    }
1051
1052    /// A press lands while the first voice is still rendering: the abandoned
1053    /// render's chunk is dropped rather than played, so the reader hears only
1054    /// the voice they pressed last.
1055    #[tokio::test]
1056    async fn a_chunk_from_a_press_the_reader_moved_past_is_not_played() {
1057        struct Gated {
1058            requests: Arc<Mutex<Vec<Option<String>>>>,
1059            release: Arc<tokio::sync::Semaphore>,
1060        }
1061        impl Engine for Gated {
1062            fn speak(&self, request: SpeakRequest) -> mpsc::Receiver<Result<Chunk, Error>> {
1063                self.requests.lock().expect("unpoisoned").push(request.voice.clone());
1064                let (sender, receiver) = mpsc::channel(2);
1065                let release = Arc::clone(&self.release);
1066                let voice = request.voice;
1067                tokio::spawn(async move {
1068                    if voice.as_deref() == Some("af_heart") {
1069                        release.acquire().await.ok();
1070                    }
1071                    let pcm = if voice.as_deref() == Some("af_heart") { vec![1] } else { vec![2] };
1072                    let _ = sender.send(Ok(Chunk { seq: 0, sample_rate: 24_000, pcm })).await;
1073                });
1074                receiver
1075            }
1076        }
1077
1078        let requests = Arc::new(Mutex::new(Vec::new()));
1079        let release = Arc::new(tokio::sync::Semaphore::new(0));
1080        let sink = Arc::new(RecordingSink { chunks: Mutex::new(Vec::new()) });
1081        let loader: super::Loader = {
1082            let requests = Arc::clone(&requests);
1083            let release = Arc::clone(&release);
1084            Arc::new(move || {
1085                Ok(Arc::new(Gated {
1086                    requests: Arc::clone(&requests),
1087                    release: Arc::clone(&release),
1088                }) as Arc<dyn Engine>)
1089            })
1090        };
1091        let service = VoiceService::new(
1092            loader,
1093            VoiceSettings::default(),
1094            Arc::clone(&sink) as Arc<dyn AudioSink>,
1095            Arc::new(RecordingNotes { said: Mutex::new(Vec::new()) }) as Arc<dyn NoteDispatch>,
1096        );
1097        let seat = seat();
1098        service.set_listening(&seat, 1, true, false);
1099
1100        let first = tokio::spawn({
1101            let service = service.clone();
1102            let seat = seat.clone();
1103            async move { service.preview(&seat, "af_heart".to_owned()).await }
1104        });
1105        let mut started = false;
1106        for _ in 0..200 {
1107            if requests.lock().expect("unpoisoned").as_slice() == [Some("af_heart".to_owned())] {
1108                started = true;
1109                break;
1110            }
1111            tokio::time::sleep(Duration::from_millis(5)).await;
1112        }
1113        assert!(started, "the first voice renders");
1114        service.preview(&seat, "bf_emma".to_owned()).await.expect("the second press is accepted");
1115        release.add_permits(1);
1116        first.await.expect("the task joins").expect("the preview ends");
1117
1118        wait_for(&sink, 1).await;
1119        assert_eq!(
1120            sink.chunks
1121                .lock()
1122                .expect("unpoisoned")
1123                .iter()
1124                .map(|(_, chunk)| chunk.pcm.clone())
1125                .collect::<Vec<_>>(),
1126            vec![vec![2]],
1127            "only the newest press is heard",
1128        );
1129        assert_eq!(requests.lock().expect("unpoisoned").len(), 2, "both voices rendered");
1130    }
1131
1132    /// A press lands while the loop is loading an engine that then fails. The
1133    /// loop's settle must hand that voice to a fresh loop rather than clear
1134    /// it: the caller of the second press already got its `Ok`.
1135    #[tokio::test]
1136    async fn a_press_during_a_failing_load_is_rendered_rather_than_dropped() {
1137        let requests = Arc::new(Mutex::new(Vec::new()));
1138        let voices = Arc::new(Mutex::new(Vec::new()));
1139        let calls = Arc::new(AtomicUsize::new(0));
1140        let (started, began) = std::sync::mpsc::channel::<()>();
1141        let (release, released) = std::sync::mpsc::channel::<()>();
1142        let released = Arc::new(Mutex::new(released));
1143        let loader: super::Loader = {
1144            let requests = Arc::clone(&requests);
1145            let voices = Arc::clone(&voices);
1146            let calls = Arc::clone(&calls);
1147            Arc::new(move || {
1148                if calls.fetch_add(1, Ordering::SeqCst) == 0 {
1149                    let _ = started.send(());
1150                    released.lock().expect("unpoisoned").recv().ok();
1151                    return Err("the first load dies".to_owned());
1152                }
1153                Ok(Arc::new(StubEngine {
1154                    requests: Arc::clone(&requests),
1155                    voices: Arc::clone(&voices),
1156                    gate: None,
1157                }) as Arc<dyn Engine>)
1158            })
1159        };
1160        let service = VoiceService::new(
1161            loader,
1162            VoiceSettings::default(),
1163            Arc::new(RecordingSink { chunks: Mutex::new(Vec::new()) }) as Arc<dyn AudioSink>,
1164            Arc::new(RecordingNotes { said: Mutex::new(Vec::new()) }) as Arc<dyn NoteDispatch>,
1165        );
1166        let seat = seat();
1167        service.set_listening(&seat, 1, true, false);
1168
1169        let first = tokio::spawn({
1170            let service = service.clone();
1171            let seat = seat.clone();
1172            async move { service.preview(&seat, "af_heart".to_owned()).await }
1173        });
1174        let mut loader_started = false;
1175        for _ in 0..200 {
1176            if began.try_recv().is_ok() {
1177                loader_started = true;
1178                break;
1179            }
1180            tokio::time::sleep(Duration::from_millis(5)).await;
1181        }
1182        assert!(loader_started, "the loader starts");
1183        service.preview(&seat, "bf_emma".to_owned()).await.expect("the press is accepted");
1184        release.send(()).expect("the loader is released");
1185
1186        first.await.expect("the task joins").expect("the waiting voice renders");
1187        assert_eq!(
1188            voices.lock().expect("unpoisoned").as_slice(),
1189            &[Some("bf_emma".to_owned())],
1190            "the press during the failing load is the voice rendered",
1191        );
1192    }
1193
1194    #[tokio::test]
1195    async fn a_disabled_service_refuses_a_say() {
1196        let fixture = service_with(|| StubEngine::new(Arc::new(Mutex::new(Vec::new()))));
1197        let service = VoiceService::new(
1198            Arc::new(|| Err("no model loads in this test".to_owned())),
1199            VoiceSettings { enabled: false, ..VoiceSettings::default() },
1200            Arc::clone(&fixture.sink) as Arc<dyn AudioSink>,
1201            Arc::clone(&fixture.notes) as Arc<dyn NoteDispatch>,
1202        );
1203        service.set_listening(&seat(), 1, true, false);
1204        assert_eq!(service.say(&seat(), "Hello.".to_owned()).await, Err(SayError::Disabled));
1205    }
1206
1207    #[tokio::test]
1208    async fn a_chunk_error_reaches_the_caller() {
1209        struct Failing;
1210        impl Engine for Failing {
1211            fn speak(&self, _request: SpeakRequest) -> mpsc::Receiver<Result<Chunk, Error>> {
1212                let (sender, receiver) = mpsc::channel(1);
1213                tokio::spawn(async move {
1214                    let _ = sender.send(Err(Error::Render("the render died".to_owned()))).await;
1215                });
1216                receiver
1217            }
1218        }
1219        let sink = Arc::new(RecordingSink { chunks: Mutex::new(Vec::new()) });
1220        let notes = Arc::new(RecordingNotes { said: Mutex::new(Vec::new()) });
1221        let service = VoiceService::new(
1222            Arc::new(|| Ok(Arc::new(Failing) as Arc<dyn Engine>)),
1223            VoiceSettings::default(),
1224            Arc::clone(&sink) as Arc<dyn AudioSink>,
1225            Arc::clone(&notes) as Arc<dyn NoteDispatch>,
1226        );
1227        service.set_listening(&seat(), 1, true, false);
1228        let outcome = service.say(&seat(), "Hello.".to_owned()).await;
1229        assert!(
1230            matches!(&outcome, Err(SayError::Engine(reason)) if reason.contains("the render died")),
1231            "the caller hears the engine's own failure, got: {outcome:?}",
1232        );
1233    }
1234
1235    #[tokio::test]
1236    async fn the_queue_stops_when_the_last_listener_leaves() {
1237        let gate = Arc::new(tokio::sync::Semaphore::new(0));
1238        let requests = Arc::new(Mutex::new(Vec::new()));
1239        let sink = Arc::new(RecordingSink { chunks: Mutex::new(Vec::new()) });
1240        let notes = Arc::new(RecordingNotes { said: Mutex::new(Vec::new()) });
1241        let engine = {
1242            let mut engine = StubEngine::new(Arc::clone(&requests));
1243            engine.gate = Some(Arc::clone(&gate));
1244            Arc::new(engine) as Arc<dyn Engine>
1245        };
1246        let service = VoiceService::new(
1247            Arc::new(move || Ok(Arc::clone(&engine))),
1248            VoiceSettings::default(),
1249            Arc::clone(&sink) as Arc<dyn AudioSink>,
1250            Arc::clone(&notes) as Arc<dyn NoteDispatch>,
1251        );
1252        service.set_listening(&seat(), 1, true, false);
1253        let first = tokio::spawn({
1254            let service = service.clone();
1255            async move { service.say(&seat(), "One.".to_owned()).await }
1256        });
1257        let second = tokio::spawn({
1258            let service = service.clone();
1259            async move { service.say(&seat(), "Two.".to_owned()).await }
1260        });
1261        for _ in 0..20 {
1262            tokio::time::sleep(Duration::from_millis(5)).await;
1263        }
1264        assert_eq!(requests.lock().expect("unpoisoned").clone(), vec!["One.".to_owned()]);
1265
1266        // The last listener leaves while the first render is held: the
1267        // queued utterance must not render into a seat nobody hears.
1268        service.set_listening(&seat(), 1, false, false);
1269        gate.add_permits(2);
1270        let _ = first.await.expect("joined");
1271        let _ = second.await.expect("joined");
1272        for _ in 0..20 {
1273            tokio::time::sleep(Duration::from_millis(5)).await;
1274        }
1275        assert_eq!(
1276            requests.lock().expect("unpoisoned").clone(),
1277            vec!["One.".to_owned()],
1278            "the queued utterance never starts",
1279        );
1280        assert!(sink.chunks.lock().expect("unpoisoned").is_empty(), "nothing reaches the sink");
1281    }
1282
1283    #[tokio::test]
1284    async fn an_occupant_change_tells_the_new_session_it_is_heard() {
1285        let fixture = service_with(|| StubEngine::new(Arc::new(Mutex::new(Vec::new()))));
1286        fixture.service.set_listening(&seat(), 1, true, false);
1287        fixture.service.occupant_changed(&seat());
1288        assert_eq!(
1289            fixture.notes.said.lock().expect("unpoisoned").clone(),
1290            vec![LISTENING_NOTE.to_owned(), LISTENING_NOTE.to_owned()],
1291            "the new occupant is told again",
1292        );
1293    }
1294
1295    #[tokio::test]
1296    async fn two_sentences_reach_the_sink_in_order() {
1297        let fixture = service_with(|| StubEngine::new(Arc::new(Mutex::new(Vec::new()))));
1298        fixture.service.set_listening(&seat(), 1, true, false);
1299        fixture.service.say(&seat(), "One. Two.".to_owned()).await.expect("queued");
1300        wait_for(&fixture.sink, 2).await;
1301        let requests = fixture.requests.lock().expect("unpoisoned").clone();
1302        assert_eq!(requests, vec!["One.".to_owned(), "Two.".to_owned()]);
1303        let seqs: Vec<u32> = fixture
1304            .sink
1305            .chunks
1306            .lock()
1307            .expect("unpoisoned")
1308            .iter()
1309            .map(|(_, chunk)| chunk.seq)
1310            .collect();
1311        assert_eq!(seqs, vec![0, 1], "the sink hears the sentences in render order");
1312    }
1313
1314    #[tokio::test]
1315    async fn an_utterance_past_the_bound_is_refused_by_name() {
1316        let fixture = service_with(|| StubEngine::new(Arc::new(Mutex::new(Vec::new()))));
1317        fixture.service.set_listening(&seat(), 1, true, false);
1318        let long = "word ".repeat(2_000);
1319        let outcome = fixture.service.say(&seat(), long).await;
1320        assert!(
1321            matches!(outcome, Err(SayError::TooLong(message)) if message.contains("max_utterance_seconds"))
1322        );
1323    }
1324
1325    #[tokio::test]
1326    async fn the_listener_note_is_injected_once_for_the_first_client_and_withdrawn_for_the_last() {
1327        let fixture = service_with(|| StubEngine::new(Arc::new(Mutex::new(Vec::new()))));
1328        let said = |fixture: &Fixture| fixture.notes.said.lock().expect("unpoisoned").clone();
1329
1330        fixture.service.set_listening(&seat(), 1, true, false);
1331        assert_eq!(said(&fixture), vec![LISTENING_NOTE.to_owned()]);
1332        fixture.service.set_listening(&seat(), 2, true, false);
1333        assert_eq!(
1334            said(&fixture),
1335            vec![LISTENING_NOTE.to_owned()],
1336            "the second client says nothing"
1337        );
1338        fixture.service.set_listening(&seat(), 1, false, false);
1339        assert_eq!(said(&fixture).len(), 1, "one client is still listening");
1340        fixture.service.set_listening(&seat(), 2, false, true);
1341        assert_eq!(said(&fixture), vec![LISTENING_NOTE.to_owned(), NOT_LISTENING_NOTE.to_owned()],);
1342        assert_eq!(fixture.service.listeners(&seat()), 0);
1343    }
1344
1345    #[test]
1346    fn a_note_rides_the_envelope_a_view_draws_as_information() {
1347        assert_eq!(
1348            note_envelope(LISTENING_NOTE),
1349            format!("[Voice]\n\n{LISTENING_NOTE}"),
1350            "the note is information for the reader and a message for the agent",
1351        );
1352        assert_eq!(
1353            note_envelope(NOT_LISTENING_NOTE),
1354            format!("[Voice]\n\n{NOT_LISTENING_NOTE}"),
1355            "the withdrawal rides the same envelope",
1356        );
1357    }
1358
1359    #[test]
1360    fn the_listening_note_carries_the_conversational_register() {
1361        assert!(
1362            LISTENING_NOTE
1363                .contains("Speak what they should hear as you would say it in a live conversation"),
1364            "the note names the register",
1365        );
1366        assert!(
1367            LISTENING_NOTE.contains("send each message's substance through mcp__forge__voice__say"),
1368            "the note names the tool that makes it heard",
1369        );
1370        assert!(
1371            LISTENING_NOTE
1372                .contains("keeping the spoken form simpler and shorter so it lands by ear"),
1373            "the spoken form is shorter than the page",
1374        );
1375        assert!(
1376            LISTENING_NOTE.contains("A reply that is only written is one they will not hear"),
1377            "a written-only reply is not heard",
1378        );
1379        assert!(
1380            LISTENING_NOTE.contains("When the stop note arrives, you may stop speaking."),
1381            "the note releases the agent when listening stops",
1382        );
1383    }
1384
1385    #[test]
1386    fn the_stop_note_keeps_its_title_and_releases_the_agent() {
1387        assert_eq!(
1388            NOT_LISTENING_NOTE,
1389            "The user has stopped listening to this seat.\n\nYou may stop speaking.",
1390            "the reader's row is titled by its first line and the agent is released",
1391        );
1392    }
1393
1394    /// The boot prewarm's failure is not terminal: it leaves the slot empty,
1395    /// and the first `say` loads again.
1396    #[tokio::test]
1397    async fn a_failed_warm_is_retried_by_the_first_say() {
1398        let loads = Arc::new(AtomicUsize::new(0));
1399        let requests = Arc::new(Mutex::new(Vec::new()));
1400        let service = VoiceService::new(
1401            {
1402                let loads = Arc::clone(&loads);
1403                let requests = Arc::clone(&requests);
1404                Arc::new(move || {
1405                    if loads.fetch_add(1, Ordering::SeqCst) == 0 {
1406                        return Err("the first load failed".to_owned());
1407                    }
1408                    Ok(Arc::new(StubEngine::new(Arc::clone(&requests))) as Arc<dyn Engine>)
1409                })
1410            },
1411            VoiceSettings::default(),
1412            Arc::new(RecordingSink { chunks: Mutex::new(Vec::new()) }),
1413            Arc::new(RecordingNotes { said: Mutex::new(Vec::new()) }),
1414        );
1415
1416        let warm = service.warm().await;
1417        assert!(
1418            matches!(&warm, Err(SayError::Engine(reason)) if reason.contains("the first load failed")),
1419            "the prewarm's own failure names the load, got: {warm:?}",
1420        );
1421
1422        service.set_listening(&seat(), 1, true, false);
1423        assert_eq!(
1424            service.say(&seat(), "Hello.".to_owned()).await,
1425            Ok(()),
1426            "the first say retries the failed load",
1427        );
1428        assert_eq!(
1429            loads.load(Ordering::SeqCst),
1430            2,
1431            "the load ran twice: the failed warm, then the say",
1432        );
1433    }
1434
1435    #[tokio::test]
1436    async fn a_paused_last_listener_says_nothing() {
1437        let fixture = service_with(|| StubEngine::new(Arc::new(Mutex::new(Vec::new()))));
1438        let said = |fixture: &Fixture| fixture.notes.said.lock().expect("unpoisoned").clone();
1439
1440        fixture.service.set_listening(&seat(), 1, true, false);
1441        assert_eq!(said(&fixture), vec![LISTENING_NOTE.to_owned()]);
1442
1443        // A seat change is a pause, not a goodbye: the last listener leaves
1444        // without an explicit off, so the agent is not told the user stopped.
1445        fixture.service.set_listening(&seat(), 1, false, false);
1446        assert_eq!(said(&fixture), vec![LISTENING_NOTE.to_owned()], "a pause fires no note");
1447        assert_eq!(fixture.service.listeners(&seat()), 0);
1448
1449        // The note survives the pause, so a return does not tell the agent
1450        // it came back.
1451        fixture.service.set_listening(&seat(), 1, true, false);
1452        assert_eq!(
1453            said(&fixture),
1454            vec![LISTENING_NOTE.to_owned()],
1455            "a return after a pause says nothing",
1456        );
1457    }
1458
1459    #[tokio::test]
1460    async fn a_dropped_connection_leaves_every_seat_it_listened_to() {
1461        let fixture = service_with(|| StubEngine::new(Arc::new(Mutex::new(Vec::new()))));
1462        let first = seat();
1463        let second = SessionSlot::new("Org", "project", "worker");
1464        fixture.service.set_listening(&first, 1, true, false);
1465        fixture.service.set_listening(&first, 2, true, false);
1466        fixture.service.set_listening(&second, 2, true, false);
1467
1468        // A socket dies without a goodbye: the core's teardown is the only
1469        // thing that can take its listener off, and it must leave every seat
1470        // the connection was on, not just the first one it finds.
1471        let changed = fixture.service.release(2);
1472        assert_eq!(changed.len(), 2, "both seats' counts moved");
1473        assert!(changed.contains(&first) && changed.contains(&second), "got: {changed:?}");
1474        assert_eq!(fixture.service.listeners(&first), 1);
1475        assert_eq!(fixture.service.listeners(&second), 0);
1476        assert_eq!(
1477            fixture.notes.said.lock().expect("unpoisoned").len(),
1478            3,
1479            "two listening notes and one withdrawal: the first seat still has a listener",
1480        );
1481        assert_eq!(
1482            fixture.notes.said.lock().expect("unpoisoned").last().map(String::as_str),
1483            Some(NOT_LISTENING_NOTE),
1484        );
1485
1486        fixture.service.release(1);
1487        assert_eq!(fixture.service.listeners(&first), 0);
1488        assert_eq!(
1489            fixture.notes.said.lock().expect("unpoisoned").last().map(String::as_str),
1490            Some(NOT_LISTENING_NOTE),
1491        );
1492        assert!(fixture.service.release(1).is_empty(), "a second release changes nothing");
1493    }
1494
1495    #[tokio::test]
1496    async fn a_second_say_queues_behind_the_first() {
1497        let gate = Arc::new(tokio::sync::Semaphore::new(0));
1498        let requests = Arc::new(Mutex::new(Vec::new()));
1499        let sink = Arc::new(RecordingSink { chunks: Mutex::new(Vec::new()) });
1500        let notes = Arc::new(RecordingNotes { said: Mutex::new(Vec::new()) });
1501        let engine = {
1502            let mut engine = StubEngine::new(Arc::clone(&requests));
1503            engine.gate = Some(Arc::clone(&gate));
1504            Arc::new(engine) as Arc<dyn Engine>
1505        };
1506        let service = VoiceService::new(
1507            Arc::new(move || Ok(Arc::clone(&engine))),
1508            VoiceSettings::default(),
1509            Arc::clone(&sink) as Arc<dyn AudioSink>,
1510            Arc::clone(&notes) as Arc<dyn NoteDispatch>,
1511        );
1512        service.set_listening(&seat(), 1, true, false);
1513
1514        let first = tokio::spawn({
1515            let service = service.clone();
1516            async move { service.say(&seat(), "One.".to_owned()).await }
1517        });
1518        let second = tokio::spawn({
1519            let service = service.clone();
1520            async move { service.say(&seat(), "Two.".to_owned()).await }
1521        });
1522        // Let both reach the seat's queue while the first render is held.
1523        for _ in 0..20 {
1524            tokio::time::sleep(Duration::from_millis(5)).await;
1525        }
1526        assert_eq!(
1527            requests.lock().expect("unpoisoned").clone(),
1528            vec!["One.".to_owned()],
1529            "the second utterance has not started while the first renders",
1530        );
1531
1532        gate.add_permits(2);
1533        first.await.expect("joined").expect("queued");
1534        second.await.expect("joined").expect("queued");
1535        wait_for(&sink, 2).await;
1536        assert_eq!(
1537            requests.lock().expect("unpoisoned").clone(),
1538            vec!["One.".to_owned(), "Two.".to_owned()],
1539        );
1540    }
1541
1542    /// The engine is one instance for the process's life: the last listener
1543    /// leaving stops the queue, not the engine. The clock is advanced past
1544    /// the old idle window, which is the change that would take it.
1545    #[tokio::test(start_paused = true)]
1546    async fn the_engine_stays_loaded_after_the_last_listener_leaves() {
1547        struct Dropped(Arc<AtomicUsize>);
1548        impl Drop for Dropped {
1549            fn drop(&mut self) {
1550                self.0.fetch_add(1, Ordering::SeqCst);
1551            }
1552        }
1553        struct WarmEngine {
1554            _dropped: Dropped,
1555        }
1556        impl Engine for WarmEngine {
1557            fn speak(&self, _request: SpeakRequest) -> mpsc::Receiver<Result<Chunk, Error>> {
1558                let (sender, receiver) = mpsc::channel(1);
1559                tokio::spawn(async move {
1560                    let _ =
1561                        sender.send(Ok(Chunk { seq: 0, sample_rate: 24_000, pcm: vec![1] })).await;
1562                });
1563                receiver
1564            }
1565        }
1566
1567        let loads = Arc::new(AtomicUsize::new(0));
1568        let drops = Arc::new(AtomicUsize::new(0));
1569        let service = VoiceService::new(
1570            {
1571                let loads = Arc::clone(&loads);
1572                let drops = Arc::clone(&drops);
1573                Arc::new(move || {
1574                    loads.fetch_add(1, Ordering::SeqCst);
1575                    Ok(Arc::new(WarmEngine { _dropped: Dropped(Arc::clone(&drops)) })
1576                        as Arc<dyn Engine>)
1577                })
1578            },
1579            VoiceSettings::default(),
1580            Arc::new(RecordingSink { chunks: Mutex::new(Vec::new()) }),
1581            Arc::new(RecordingNotes { said: Mutex::new(Vec::new()) }),
1582        );
1583        service.set_listening(&seat(), 1, true, false);
1584        service.say(&seat(), "One.".to_owned()).await.expect("rendered");
1585        assert_eq!(loads.load(Ordering::SeqCst), 1, "premise: the engine loaded");
1586
1587        service.set_listening(&seat(), 1, false, true);
1588        // Let a release task reach its sleep before the clock jumps, or it
1589        // registers its deadline after the jump and the mutation survives.
1590        for _ in 0..5 {
1591            tokio::task::yield_now().await;
1592        }
1593        tokio::time::advance(Duration::from_secs(10 * 60)).await;
1594        for _ in 0..20 {
1595            tokio::task::yield_now().await;
1596        }
1597        assert_eq!(drops.load(Ordering::SeqCst), 0, "the engine outlives the last listener");
1598        assert!(
1599            service.engine.lock().await.is_some(),
1600            "the parked engine stays in the slot after the seat empties",
1601        );
1602
1603        service.set_listening(&seat(), 1, true, false);
1604        service.say(&seat(), "Two.".to_owned()).await.expect("rendered");
1605        assert_eq!(loads.load(Ordering::SeqCst), 1, "the next say reuses the parked engine");
1606    }
1607
1608    fn prewarm_workspace(tts_enabled: bool) -> (Arc<crate::Workspace>, tempfile::TempDir) {
1609        let dir = tempfile::tempdir().expect("a temp dir");
1610        let mut config = crate::config::LoadedConfig::empty_for_test();
1611        config.tts.enabled = tts_enabled;
1612        let (workspace, _updates) =
1613            crate::Workspace::testing_stub_with_config(dir.path().to_path_buf(), config)
1614                .expect("the stub builds");
1615        workspace.install_voice_engine_for_test(Arc::new(StubEngine::new(Arc::new(Mutex::new(
1616            Vec::new(),
1617        )))));
1618        (workspace, dir)
1619    }
1620
1621    /// The boot prewarm loads with `[tts]` on: the stub engine reaches the
1622    /// slot. Deleting the dispatch or its spawn leaves this red, which the
1623    /// wave-asked guard alone cannot see.
1624    #[tokio::test]
1625    async fn the_boot_prewarm_loads_the_engine_when_tts_is_on() {
1626        let (workspace, _dir) = prewarm_workspace(true);
1627        workspace.start_voice_prewarm();
1628        assert!(workspace.test_voice_prewarm_started(), "the wave asked for it");
1629
1630        let service = workspace.voice_service();
1631        for _ in 0..200 {
1632            if service.engine.lock().await.is_some() {
1633                return;
1634            }
1635            tokio::time::sleep(Duration::from_millis(5)).await;
1636        }
1637        panic!("the boot prewarm did not load the engine");
1638    }
1639
1640    /// The dispatch guard is not the wave-asked signal: a first call with
1641    /// `[tts]` off spends only the signal, and a second call still loads.
1642    #[tokio::test]
1643    async fn a_prewarm_call_after_tts_turns_on_dispatches() {
1644        let (workspace, _dir) = prewarm_workspace(false);
1645        workspace.start_voice_prewarm();
1646        assert!(workspace.test_voice_prewarm_started(), "the wave asked");
1647        assert!(
1648            workspace.voice_service().engine.lock().await.is_none(),
1649            "nothing loads while the section is off",
1650        );
1651
1652        let mut enabled = crate::config::LoadedConfig::empty_for_test();
1653        enabled.tts.enabled = true;
1654        *workspace.config.write() = Arc::new(enabled);
1655
1656        workspace.start_voice_prewarm();
1657        let service = workspace.voice_service();
1658        for _ in 0..200 {
1659            if service.engine.lock().await.is_some() {
1660                return;
1661            }
1662            tokio::time::sleep(Duration::from_millis(5)).await;
1663        }
1664        panic!("the second call did not dispatch the load");
1665    }
1666
1667    /// A listener can leave while the engine loads: the say answers the
1668    /// empty seat rather than a `NotReady` that blames the engine.
1669    #[tokio::test]
1670    async fn a_say_whose_listener_leaves_during_the_load_answers_no_listener() {
1671        let (release, hold) = std::sync::mpsc::channel::<()>();
1672        let hold = Arc::new(Mutex::new(hold));
1673        let loads = Arc::new(AtomicUsize::new(0));
1674        let requests = Arc::new(Mutex::new(Vec::new()));
1675        let service = VoiceService::new(
1676            {
1677                let hold = Arc::clone(&hold);
1678                let loads = Arc::clone(&loads);
1679                let requests = Arc::clone(&requests);
1680                Arc::new(move || {
1681                    loads.fetch_add(1, Ordering::SeqCst);
1682                    let _ = hold.lock().expect("unpoisoned").recv();
1683                    Ok(Arc::new(StubEngine::new(Arc::clone(&requests))) as Arc<dyn Engine>)
1684                })
1685            },
1686            VoiceSettings::default(),
1687            Arc::new(RecordingSink { chunks: Mutex::new(Vec::new()) }),
1688            Arc::new(RecordingNotes { said: Mutex::new(Vec::new()) }),
1689        );
1690        service.set_listening(&seat(), 1, true, false);
1691        let say = tokio::spawn({
1692            let service = service.clone();
1693            async move { service.say(&seat(), "Hello.".to_owned()).await }
1694        });
1695        for _ in 0..200 {
1696            if loads.load(Ordering::SeqCst) == 1 {
1697                break;
1698            }
1699            tokio::time::sleep(Duration::from_millis(5)).await;
1700        }
1701        assert_eq!(loads.load(Ordering::SeqCst), 1, "premise: the say is mid-load");
1702
1703        service.set_listening(&seat(), 1, false, true);
1704        release.send(()).expect("the loader is waiting");
1705        assert_eq!(
1706            say.await.expect("joined"),
1707            Err(SayError::NoListener),
1708            "the seat emptied, so the say answers the seat, not the engine",
1709        );
1710    }
1711
1712    /// A `say` that lands while the boot prewarm is still loading waits for
1713    /// that load: the engine is never released underneath a say, and a load
1714    /// in flight is not answered as `NotReady`.
1715    #[tokio::test]
1716    async fn a_say_waits_for_the_prewarm_instead_of_answering_not_ready() {
1717        // A dropped sender releases the load, so a failing assertion unwinds
1718        // instead of leaving the blocking loader parked for ever.
1719        let (release, hold) = std::sync::mpsc::channel::<()>();
1720        let hold = Arc::new(Mutex::new(hold));
1721        let loads = Arc::new(AtomicUsize::new(0));
1722        let requests = Arc::new(Mutex::new(Vec::new()));
1723        let service = VoiceService::new(
1724            {
1725                let hold = Arc::clone(&hold);
1726                let loads = Arc::clone(&loads);
1727                let requests = Arc::clone(&requests);
1728                Arc::new(move || {
1729                    loads.fetch_add(1, Ordering::SeqCst);
1730                    let _ = hold.lock().expect("unpoisoned").recv();
1731                    Ok(Arc::new(StubEngine::new(Arc::clone(&requests))) as Arc<dyn Engine>)
1732                })
1733            },
1734            VoiceSettings::default(),
1735            Arc::new(RecordingSink { chunks: Mutex::new(Vec::new()) }),
1736            Arc::new(RecordingNotes { said: Mutex::new(Vec::new()) }),
1737        );
1738        service.set_listening(&seat(), 1, true, false);
1739        let warm = tokio::spawn({
1740            let service = service.clone();
1741            async move { service.warm().await }
1742        });
1743        for _ in 0..200 {
1744            if loads.load(Ordering::SeqCst) == 1 {
1745                break;
1746            }
1747            tokio::time::sleep(Duration::from_millis(5)).await;
1748        }
1749        assert_eq!(loads.load(Ordering::SeqCst), 1, "premise: the prewarm is mid-load");
1750
1751        let say = tokio::spawn({
1752            let service = service.clone();
1753            async move { service.say(&seat(), "Hello.".to_owned()).await }
1754        });
1755        for _ in 0..50 {
1756            tokio::time::sleep(Duration::from_millis(2)).await;
1757        }
1758        assert!(!say.is_finished(), "premise: the say is waiting on the load, not answered");
1759
1760        release.send(()).expect("the loader is waiting");
1761        warm.await.expect("joined").expect("warmed");
1762        assert_eq!(
1763            say.await.expect("joined"),
1764            Ok(()),
1765            "the say waits for the prewarm instead of answering NotReady",
1766        );
1767        assert_eq!(loads.load(Ordering::SeqCst), 1, "one load served both");
1768    }
1769
1770    #[tokio::test]
1771    async fn says_that_arrive_during_the_load_queue_behind_it() {
1772        let loads = Arc::new(AtomicUsize::new(0));
1773        let requests = Arc::new(Mutex::new(Vec::new()));
1774        let sink = Arc::new(RecordingSink { chunks: Mutex::new(Vec::new()) });
1775        let notes = Arc::new(RecordingNotes { said: Mutex::new(Vec::new()) });
1776        let service = VoiceService::new(
1777            {
1778                let loads = Arc::clone(&loads);
1779                let requests = Arc::clone(&requests);
1780                Arc::new(move || {
1781                    loads.fetch_add(1, Ordering::SeqCst);
1782                    std::thread::sleep(Duration::from_millis(50));
1783                    Ok(Arc::new(StubEngine::new(Arc::clone(&requests))) as Arc<dyn Engine>)
1784                })
1785            },
1786            VoiceSettings::default(),
1787            Arc::clone(&sink) as Arc<dyn AudioSink>,
1788            Arc::clone(&notes) as Arc<dyn NoteDispatch>,
1789        );
1790        let seat = seat();
1791        service.set_listening(&seat, 1, true, false);
1792
1793        let (first, second) = tokio::join!(
1794            service.say(&seat, "One.".to_owned()),
1795            service.say(&seat, "Two.".to_owned())
1796        );
1797        assert_eq!(first, Ok(()));
1798        assert_eq!(second, Ok(()));
1799        assert_eq!(loads.load(Ordering::SeqCst), 1, "the second say waits for the load");
1800        wait_for(&sink, 2).await;
1801    }
1802}