Skip to main content

leviath_runtime/host/
mod.rs

1//! The world host: the daemon-side wrapper that owns a single [`PipelineWorld`],
2//! maps stable **run ids** to ECS entities, and interleaves external **control
3//! operations** with driving the world - all on one task, so there is never any
4//! locking around the world.
5//!
6//! Clients (a control socket, the TUI, the CLI) don't hold entities - those are
7//! generational indices meaningful only inside the world. They address agents by
8//! run id. The host keeps the `run_id → Entity` map and turns each
9//! [`ControlOp`] into the corresponding [`PipelineWorld`] call, replying on the
10//! op's oneshot channel.
11//!
12//! The serve loop drives the world to quiescence, then parks until either an
13//! async result wakes it, a control op arrives, or shutdown is signalled -
14//! handling a control op and then re-driving to quiescence so its effect (a
15//! resume, a delivered message) is applied immediately.
16
17use std::collections::{HashMap, HashSet, VecDeque};
18use std::time::Duration;
19
20use bevy_ecs::entity::Entity;
21use tokio::sync::broadcast;
22use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
23
24use crate::components::{
25    AgentMessage, AgentState, AgentStatus, AwaitingInteraction, ContextWindow, ParentRef,
26    SubAgentChildren, WaitReason,
27};
28use crate::interaction_hub::InteractionHub;
29use crate::persistence::{RunMetadata, TokenTotals};
30use crate::world::{AgentId, LaneSnapshot, PipelineWorld};
31
32// Sections of the former single-file host, one per concern. Glob re-exported so
33// every existing `host::ControlOp` / `host::WorldEvent` path keeps working and
34// the split stays a pure move.
35mod events;
36pub use events::*;
37mod types;
38pub use types::*;
39
40/// Owns the world and the run-id map; drives the world and services control ops.
41pub struct WorldHost {
42    world: PipelineWorld,
43    by_run_id: HashMap<String, AgentId>,
44    interactions: InteractionHub,
45    spawner: Option<Spawner>,
46    spawn_preprocessor: Option<SpawnPreprocessor>,
47    reloader: Option<Reloader>,
48    force_terminator: Option<ForceTerminator>,
49    reaper: Option<Reaper>,
50    events: broadcast::Sender<WorldEvent>,
51    emitted: HashMap<String, Emitted>,
52    emitted_interactions: HashSet<String>,
53    /// Sub-agent world-access requests from tool lanes. The host holds a `tx`
54    /// clone so the receiver never closes (its `recv` never yields `None`).
55    subagent_tx: UnboundedSender<SubAgentOp>,
56    subagent_rx: UnboundedReceiver<SubAgentOp>,
57    /// How often [`Self::serve`] re-drives the world even though nothing woke
58    /// it. See [`Self::set_redrive_interval`].
59    redrive: Duration,
60    /// Consecutive re-drives that found the lanes full and nothing moved. See
61    /// [`Self::observe_redrive`].
62    dead_cycles: u32,
63    /// The progress fingerprint as of the previous re-drive, or `None` before
64    /// the first one.
65    last_progress: Option<u64>,
66    /// Extra tool-lane permits the relief valve has handed out and not yet
67    /// reclaimed (see [`Self::decay_relief_if_healthy`]).
68    relief_granted: usize,
69    /// Consecutive re-drives that found the lane healthy (no dead cycles, no
70    /// queue) while relief was outstanding - the decay countdown.
71    healthy_cycles: u32,
72    /// Dead cycles the daemon tolerates before widening the tool lane. `0`
73    /// disables relief. See [`Self::set_dead_cycles_before_relief`].
74    dead_cycles_before_relief: u32,
75    /// Runs unloaded recently enough to still be worth reporting, oldest first,
76    /// each paired with the unix second it was unloaded. See
77    /// [`Self::record_finished`].
78    finished: VecDeque<(i64, RunListEntry)>,
79    /// How long an unloaded run stays in [`Self::finished`]. `0` keeps none.
80    /// See [`Self::set_finished_retention_secs`].
81    finished_retention_secs: u64,
82    /// Paused runs the host has paged out of the world, by run id, each holding
83    /// its last listing row. A parked run's full state is on disk; `Resume`,
84    /// `Message` and `Cancel` all page it back through
85    /// [`Self::resolve_or_reload`], and [`Self::list`] keeps reporting it so an
86    /// operator's `lev ps` view does not change just because the daemon stopped
87    /// spending memory on a run nobody is driving.
88    parked: HashMap<String, RunListEntry>,
89}
90
91/// Consecutive healthy re-drives (no dead cycles, empty tool queue) before the
92/// relief-decay valve reclaims one granted permit. Each re-drive is seconds
93/// apart, so four of them is a comfortably-over margin - and each further
94/// healthy cycle reclaims one more, so a full lane's worth drains in minutes.
95const HEALTHY_CYCLES_BEFORE_DECAY: u32 = 4;
96
97/// How often the serve loop re-drives the world on its own.
98///
99/// The loop is event-driven, so a missed wake anywhere parks it indefinitely -
100/// the daemon looks alive while nothing progresses, which is what issue #189
101/// reported as hours of frozen agents. This bounds any such wedge to one
102/// interval instead of "until something unrelated happens", and gives the lane
103/// heartbeat a place to run.
104///
105/// Deliberately not configurable: it is a correctness backstop, not a tuning
106/// knob. A no-op re-drive is one tick over a handful of systems plus an event
107/// diff, so at this cadence it costs nothing measurable.
108const DEFAULT_REDRIVE_INTERVAL: Duration = Duration::from_secs(30);
109
110/// How many consecutive dead cycles trigger the tool-lane relief valve.
111///
112/// At the 30-second re-drive that is five minutes of a full lane going nowhere -
113/// long enough that ordinary backpressure never reaches it, short enough that a
114/// genuinely wedged daemon is not left overnight. Served from
115/// `[limits] dead_cycles_before_relief`; `0` disables relief.
116pub const DEFAULT_DEAD_CYCLES_BEFORE_RELIEF: u32 = 10;
117
118/// How long a run stays in the listing after the daemon unloads it.
119///
120/// A terminal agent is unloaded a pass or two after it finishes, and until now
121/// it vanished from the listing at that moment. A run that died on its first
122/// inference was therefore indistinguishable from one that had never been
123/// spawned, which is what left the scheduler in issue #205 with nothing to go on
124/// but a stopwatch: it could not tell a dead spawn from a slow one, so it
125/// reverted the work and spawned again, for forty minutes.
126///
127/// Five minutes covers several polls of any scheduler that checks in about once
128/// a minute, so a single missed or slow poll does not lose the evidence. It is
129/// also what the rest of the daemon already means by "long enough that a hiccup
130/// cannot cause it": the dashboard calls a run stale at 300 seconds, and
131/// [`DEFAULT_DEAD_CYCLES_BEFORE_RELIEF`] at the 30-second re-drive works out to
132/// the same five minutes.
133///
134/// Served from `[limits] finished_retention_secs`; `0` keeps nothing and
135/// restores the old behaviour.
136pub const DEFAULT_FINISHED_RETENTION_SECS: u64 = 300;
137
138/// How many unloaded runs [`WorldHost::finished`] holds before the oldest are
139/// dropped, whatever the retention window says.
140///
141/// Not configurable: it is a memory bound, not a tuning knob. A factory that
142/// finishes runs faster than this fills the window keeps the most recent ones,
143/// which are the ones anyone is still asking about. Set the window shorter to
144/// control how much the listing shows; this only stops it growing without end.
145const MAX_RETAINED_FINISHED: usize = 256;
146
147impl WorldHost {
148    /// Wrap a world with a fresh interaction hub.
149    pub fn new(world: PipelineWorld) -> Self {
150        Self::with_interactions(world, InteractionHub::new())
151    }
152
153    /// Wrap a world with a specific interaction hub - the daemon shares one hub
154    /// between the tool service's per-agent backends and this host.
155    pub fn with_interactions(mut world: PipelineWorld, interactions: InteractionHub) -> Self {
156        // 256, not more: a tokio broadcast ring never shrinks, so every slot a
157        // busy period fills stays allocated (holding its event's strings) for
158        // the daemon's life. Consumers here are live relays, not replayers -
159        // one that falls a full ring behind gets a Lagged skip either way.
160        let (events, _) = broadcast::channel(256);
161        // Let ECS systems (the persistence drain) push events - per-agent log
162        // lines - into the same stream the control transport serves.
163        world
164            .world_mut()
165            .insert_resource(WorldEventSink(events.clone()));
166        let (subagent_tx, subagent_rx) = tokio::sync::mpsc::unbounded_channel();
167        Self {
168            world,
169            by_run_id: HashMap::new(),
170            interactions,
171            spawner: None,
172            spawn_preprocessor: None,
173            reloader: None,
174            force_terminator: None,
175            reaper: None,
176            events,
177            emitted: HashMap::new(),
178            emitted_interactions: HashSet::new(),
179            parked: HashMap::new(),
180            subagent_tx,
181            subagent_rx,
182            redrive: DEFAULT_REDRIVE_INTERVAL,
183            dead_cycles: 0,
184            last_progress: None,
185            relief_granted: 0,
186            healthy_cycles: 0,
187            dead_cycles_before_relief: DEFAULT_DEAD_CYCLES_BEFORE_RELIEF,
188            finished: VecDeque::new(),
189            finished_retention_secs: DEFAULT_FINISHED_RETENTION_SECS,
190        }
191    }
192}
193
194// Sections of the former single-file host impl, one per concern. An inherent
195// impl may live in any module of the defining crate, so each file below carries
196// its own `impl WorldHost` block rather than a trait or a free function.
197mod emit;
198mod health;
199mod listing;
200mod subagents;
201
202impl WorldHost {
203    /// Register any agent that exists in the world but is missing from the run-id
204    /// map, so the host's view is the world's view.
205    ///
206    /// Not every agent arrives through a `Spawn` control op: fan-out workers are
207    /// built straight into the world by the fan-out spawner, which has no handle
208    /// on the host to register them. An unregistered agent is invisible to `list`
209    /// (so `lev ps` never showed a worker), never reaped (its sandbox and tool
210    /// state leak), and - worst - un-cancellable, because a cancel by its run id
211    /// misses the map, falls through to the reloader, and pages a **second** live
212    /// entity in from that run's on-disk state while the original keeps running.
213    /// Adopting them here is idempotent and keeps a stale mapping from winning:
214    /// a registered id whose entity has been despawned is re-pointed.
215    fn adopt_unregistered_runs(&mut self) {
216        let live: Vec<(String, Entity)> = self
217            .world
218            .world_mut()
219            .query::<(Entity, &RunMetadata)>()
220            .iter(self.world.world())
221            .map(|(entity, md)| (md.run_id.clone(), entity))
222            .collect();
223        for (run_id, entity) in live {
224            // Straight out of this world's query, so it is ours by construction.
225            let agent = self.world.own_agent(entity);
226            if self.live_entity(&run_id) != Some(agent) {
227                self.by_run_id.insert(run_id, agent);
228            }
229        }
230    }
231
232    /// Whether a paused agent is safe to page out of the world.
233    ///
234    /// Conservative on purpose - this is the restart-equivalence question, and
235    /// only shapes where the answer is a settled "yes" qualify:
236    /// - status is `Paused`, and the *persisted* status is too (the watermark
237    ///   proves the paused snapshot was dispatched, so disk can rebuild it);
238    /// - it is a standalone root: no parent that might address it by entity,
239    ///   no children whose links a page-in would have to rebuild;
240    /// - no open interaction and no fan-out in flight (a pause that landed
241    ///   mid-prompt or mid-split keeps its live machinery).
242    fn parkable(&self, entity: Entity, status: &AgentStatus) -> bool {
243        if !matches!(status, AgentStatus::Paused) {
244            return false;
245        }
246        // No reloader, no parking: a host that cannot page a run back in
247        // (an embedded world, a bare test host) must keep it resident, or
248        // "paused" silently becomes "gone".
249        if self.reloader.is_none() {
250            return false;
251        }
252        let world = self.world.world();
253        let paused_persisted = world
254            .get::<crate::pipeline::PersistWatermark>(entity)
255            .and_then(|w| w.persisted_status())
256            == Some(leviath_core::run_meta::RunStatus::Paused);
257        paused_persisted
258            && world.get::<crate::components::ParentRef>(entity).is_none()
259            && world.get::<SubAgentChildren>(entity).is_none()
260            && world.get::<crate::fanout::FanOutWaiting>(entity).is_none()
261            && world
262                .get::<crate::interaction_points::AwaitingInteractionPoint>(entity)
263                .is_none()
264            && world.get::<AwaitingInteraction>(entity).is_none()
265    }
266
267    /// Whether a terminal agent is safe to unload: it has no **live** parent that
268    /// might still be waiting on it. True for a root (no `ParentRef`), or when its
269    /// parent has been despawned or is itself terminal; false while a non-terminal
270    /// parent could still be gating on this child.
271    fn no_live_parent(&self, entity: Entity) -> bool {
272        let world = self.world.world();
273        match world.get::<crate::components::ParentRef>(entity) {
274            None => true,
275            Some(parent_ref) => match world.get::<AgentState>(parent_ref.parent_entity) {
276                None => true,
277                Some(state) => matches!(
278                    state.status,
279                    AgentStatus::Complete | AgentStatus::Error { .. } | AgentStatus::Cancelled
280                ),
281            },
282        }
283    }
284
285    /// Install the spawner used to service `Spawn` control ops. Without one, a
286    /// `Spawn` op replies with an error.
287    pub fn set_spawner(&mut self, spawner: Spawner) {
288        self.spawner = Some(spawner);
289    }
290
291    /// Install the async hook awaited before each top-level `Spawn` (see
292    /// [`SpawnPreprocessor`]).
293    pub fn set_spawn_preprocessor(&mut self, pp: SpawnPreprocessor) {
294        self.spawn_preprocessor = Some(pp);
295    }
296
297    /// Install the reloader used to page an unloaded run back in on demand.
298    /// Without one, an op targeting a run that isn't in memory just misses.
299    pub fn set_reloader(&mut self, reloader: Reloader) {
300        self.reloader = Some(reloader);
301    }
302
303    /// Install the [`ForceTerminator`] used to terminate a run on disk when the
304    /// world cannot hold it. Without one, a cancel that misses in the world and
305    /// can't be reloaded just misses.
306    pub fn set_force_terminator(&mut self, force_terminator: ForceTerminator) {
307        self.force_terminator = Some(force_terminator);
308    }
309
310    /// Install the reap hook run just before each terminal agent is despawned,
311    /// so the daemon can tear down that agent's sandbox and drop its tool state.
312    /// Without one, reaping just despawns the entity (the prior behavior).
313    pub fn set_reaper(&mut self, reaper: Reaper) {
314        self.reaper = Some(reaper);
315    }
316
317    /// Resolve a run id to a live entity, paging it in from disk if it has been
318    /// unloaded (and a reloader is installed). Returns `None` if the run is
319    /// neither live nor resumable from disk. Newly-reloaded runs are registered.
320    fn resolve_or_reload(&mut self, run_id: &str) -> Option<AgentId> {
321        if let Some(entity) = self.live_entity(run_id) {
322            return Some(entity);
323        }
324        let entity = (self.reloader.as_mut()?)(&mut self.world, run_id)?;
325        self.by_run_id.insert(run_id.to_string(), entity);
326        // Live again: its listing row comes off the entity, not the parked map.
327        self.parked.remove(run_id);
328        Some(entity)
329    }
330
331    /// A clone of the interaction hub, for building per-agent backends.
332    pub fn interactions(&self) -> InteractionHub {
333        self.interactions.clone()
334    }
335
336    /// Mutable access to the underlying world (for the spawner to add agents).
337    pub fn world_mut(&mut self) -> &mut PipelineWorld {
338        &mut self.world
339    }
340
341    /// Record the run-id → entity mapping for a freshly-spawned agent.
342    pub fn register(&mut self, run_id: impl Into<String>, agent: AgentId) {
343        let run_id = run_id.into();
344        self.parked.remove(&run_id);
345        self.by_run_id.insert(run_id, agent);
346    }
347
348    /// Resolve a run id to a **live** entity (one that still exists in the world).
349    fn live_entity(&self, run_id: &str) -> Option<AgentId> {
350        let agent = *self.by_run_id.get(run_id)?;
351        self.world
352            .world()
353            .get::<AgentState>(agent.entity())
354            .map(|_| agent)
355    }
356
357    /// Apply one control op and reply on its channel. A dropped reply receiver is
358    /// harmless (the requester went away).
359    pub fn handle(&mut self, op: ControlOp) {
360        match op {
361            ControlOp::Spawn { args, reply } => {
362                let result = match self.spawner.as_mut() {
363                    // Spawning runs outside the pipeline schedule, so it isn't
364                    // covered by `run_isolated`'s panic guard: a panic while
365                    // parsing a blueprint or building a sandbox would otherwise
366                    // unwind the whole serve task and take the daemon with it.
367                    // As with `run_isolated`, the world may be left holding a
368                    // partially-built entity - the run just never registers.
369                    Some(spawner) => {
370                        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
371                            spawner(&mut self.world, &args)
372                        })) {
373                            Ok(Ok(entity)) => {
374                                // Spawned into this world, so it is ours.
375                                let agent = self.world.own_agent(entity);
376                                self.by_run_id.insert(args.run_id.clone(), agent);
377                                Ok(args.run_id.clone())
378                            }
379                            Ok(Err(e)) => Err(e),
380                            Err(_) => Err("agent spawn panicked".to_string()),
381                        }
382                    }
383                    None => Err("this daemon cannot spawn agents".to_string()),
384                };
385                // A failed spawn must leave a trace daemon-side: the error goes
386                // back over the socket to a client that may have already exited,
387                // and nothing is written to disk, so without this log line the
388                // failure is invisible (issue #107).
389                if let Err(error) = &result {
390                    tracing::error!(
391                        run_id = %args.run_id,
392                        blueprint = %args.blueprint_path,
393                        workdir = %args.workdir,
394                        error = %error,
395                        "agent spawn failed"
396                    );
397                }
398                let _ = reply.send(result);
399            }
400            ControlOp::Result { run_id, reply } => {
401                // Live entities only. An unloaded run's answer is on disk in
402                // `meta.json`, which is what `lev result` reads; keeping a copy
403                // of every finished run's answer in memory would defeat the
404                // point of bounding the finished buffer.
405                let output = self
406                    .live_entity(&run_id)
407                    .and_then(|agent| {
408                        self.world
409                            .world()
410                            .get::<crate::persistence::FinalOutput>(agent.entity())
411                    })
412                    .map(|o| o.0.clone());
413                let _ = reply.send(output);
414            }
415            ControlOp::Status { run_id, reply } => {
416                // A run the daemon has unloaded still has an answer for a
417                // while, so a caller that asks a moment too late learns how the
418                // run ended instead of being told there is no such run.
419                let status = self
420                    .live_entity(&run_id)
421                    .and_then(|e| self.world.agent_status(e))
422                    .or_else(|| self.parked.get(&run_id).map(|e| e.status.clone()))
423                    .or_else(|| {
424                        self.finished
425                            .iter()
426                            .find(|(_, e)| e.run_id == run_id)
427                            .map(|(_, e)| e.status.clone())
428                    });
429                let _ = reply.send(status);
430            }
431            ControlOp::Pause { run_id, reply } => {
432                let ok = self
433                    .resolve_or_reload(&run_id)
434                    .is_some_and(|e| self.world.pause(e));
435                let _ = reply.send(ok);
436            }
437            ControlOp::Resume { run_id, reply } => {
438                let ok = self
439                    .resolve_or_reload(&run_id)
440                    .is_some_and(|e| self.world.resume(e));
441                let _ = reply.send(ok);
442            }
443            ControlOp::Cancel { run_id, reply } => {
444                // Cancel is unconditional: it either takes effect in the world
445                // (root plus every descendant) or, when the run can't be held
446                // there at all, is forced onto its on-disk state. It reports
447                // `false` only when there is genuinely no such run anywhere -
448                // otherwise a run whose blueprint had moved stayed `running` on
449                // disk forever with no way to get rid of it.
450                let ok = self.cancel_tree(&run_id)
451                    || self
452                        .force_terminator
453                        .as_mut()
454                        .is_some_and(|terminate| terminate(&run_id));
455                let _ = reply.send(ok);
456            }
457            ControlOp::List { reply } => {
458                let _ = reply.send(RunListing {
459                    runs: self.list(),
460                    finished: self.finished(),
461                    health: self.health(),
462                });
463            }
464            ControlOp::Message {
465                agent_id,
466                content,
467                target_region,
468                reply,
469            } => {
470                // Page the target in if it was unloaded, so delivery finds it.
471                self.resolve_or_reload(&agent_id);
472                let ok = self
473                    .world
474                    .send_message(AgentMessage {
475                        agent_id,
476                        content,
477                        target_region,
478                    })
479                    .is_ok();
480                let _ = reply.send(ok);
481            }
482            ControlOp::ListInteractions { reply } => {
483                let _ = reply.send(self.interactions.pending());
484            }
485            ControlOp::AnswerInteraction { response, reply } => {
486                let _ = reply.send(self.interactions.answer(response));
487            }
488            ControlOp::CancelInteraction { request_id, reply } => {
489                let _ = reply.send(self.interactions.cancel(&request_id));
490            }
491            ControlOp::Shutdown { reply } => {
492                // Reply first (best effort), then trigger the world's shutdown so
493                // the serve loop's next `select!` returns.
494                let _ = reply.send(true);
495                self.world.shutdown();
496            }
497        }
498    }
499
500    /// Flush all queued persistence and stop the hosted world, guaranteeing every
501    /// dirty agent's final snapshot reaches disk (see
502    /// [`PipelineWorld::flush_and_stop`]). Invoked automatically when [`Self::serve`]
503    /// returns; also exposed directly for callers that drive the world themselves.
504    pub async fn flush_and_stop(&mut self) {
505        self.world.flush_and_stop().await;
506    }
507
508    /// Run the host: drive the world to quiescence, then park until an async
509    /// result wakes it, a control op arrives, or shutdown is signalled. Returns
510    /// when shutdown fires or the control channel closes - and before returning,
511    /// **flushes all queued persistence to disk** ([`Self::flush_and_stop`]) so a
512    /// clean daemon shutdown never loses a dirty agent's final snapshot.
513    pub async fn serve(&mut self, mut control_rx: UnboundedReceiver<ControlOp>) {
514        let wake = self.world.wake_handle();
515        let shutdown = self.world.shutdown_handle();
516        // `interval_at` rather than `interval`: the latter's first tick is
517        // immediately ready, which would spin one pointless pass at startup.
518        // `Delay` keeps a slow drive from queueing a burst of catch-up ticks.
519        let mut redrive =
520            tokio::time::interval_at(tokio::time::Instant::now() + self.redrive, self.redrive);
521        redrive.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
522        'serve: loop {
523            self.world.run_to_fixed_point();
524            self.emit_events();
525            tokio::select! {
526                _ = wake.notified() => {}
527                _ = shutdown.notified() => break 'serve,
528                // The backstop. Everything else here is edge-triggered, so a
529                // release or completion that forgets to wake us would otherwise
530                // park the daemon indefinitely with work left to do. Re-driving
531                // on a timer bounds that to one interval, and is where the lane
532                // heartbeat reports what the loop is actually waiting on.
533                _ = redrive.tick() => self.observe_redrive(),
534                op = control_rx.recv() => {
535                    match op {
536                        // Await the spawn preprocessor (e.g. lazy MCP connect) before
537                        // the sync spawner runs, so the pool is warm. The returned
538                        // future is `'static`, so no borrow of `self`/`op` outlives it.
539                        Some(op) => {
540                            let pre = match &op {
541                                ControlOp::Spawn { args, .. } => {
542                                    self.spawn_preprocessor.as_ref().map(|pp| pp(args))
543                                }
544                                _ => None,
545                            };
546                            if let Some(fut) = pre {
547                                fut.await;
548                            }
549                            self.handle(op);
550                        }
551                        None => break 'serve, // all control senders dropped
552                    }
553                }
554                // The host holds a `subagent_tx`, so this only yields `Some`.
555                Some(sub) = self.subagent_rx.recv() => {
556                    // Warm a spawning sub-agent's MCP servers first, same as a
557                    // top-level Spawn (both run in this async loop).
558                    let pre = match &sub {
559                        SubAgentOp::Spawn { args, .. } => {
560                            self.spawn_preprocessor.as_ref().map(|pp| pp(args))
561                        }
562                        _ => None,
563                    };
564                    if let Some(fut) = pre {
565                        fut.await;
566                    }
567                    self.handle_subagent(sub);
568                }
569            }
570        }
571        // Shutting down: drain the persistence lane before the world is dropped.
572        self.flush_and_stop().await;
573    }
574}
575
576#[cfg(test)]
577#[path = "../host_tests.rs"]
578mod tests;