Skip to main content

zeph_tools/shell/
background.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Background shell execution registry and associated types.
5//!
6//! This module provides the [`RunId`] newtype for tracking individual background
7//! shell runs, and the [`BackgroundHandle`] struct used by `ShellExecutor` to
8//! manage in-flight processes.
9//!
10//! Background runs are stored in a `HashMap<RunId, BackgroundHandle>` on the
11//! executor. The registry is bounded by `max_background_runs` from config.
12
13use std::time::Instant;
14
15use tokio_util::sync::CancellationToken;
16use uuid::Uuid;
17
18/// Opaque correlation identifier for a background shell run.
19///
20/// The inner field is private: external code cannot construct a `RunId` that
21/// collides with an existing registry entry. Displays as a 32-character
22/// lowercase hex string so the LLM can reference it in follow-up turns.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
24#[serde(transparent)]
25pub struct RunId(Uuid);
26
27impl RunId {
28    /// Generate a new random `RunId`.
29    pub(crate) fn new() -> Self {
30        Self(Uuid::new_v4())
31    }
32}
33
34impl std::fmt::Display for RunId {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        write!(f, "{:032x}", self.0.as_u128())
37    }
38}
39
40/// Registry entry for an in-flight background shell run.
41#[derive(Debug)]
42pub(crate) struct BackgroundHandle {
43    /// Command string, stored for shutdown reporting and TUI display.
44    pub command: String,
45    /// Wall-clock start time for elapsed reporting.
46    pub started_at: Instant,
47    /// Cancellation token. Cancel to request graceful shutdown.
48    pub abort: CancellationToken,
49    /// OS process ID, if known. Used for SIGTERM/SIGKILL escalation on Unix during shutdown.
50    pub child_pid: Option<u32>,
51}
52
53impl BackgroundHandle {
54    /// Returns the wall-clock elapsed time since this run was spawned.
55    pub(crate) fn elapsed(&self) -> std::time::Duration {
56        self.started_at.elapsed()
57    }
58}
59
60/// Lightweight snapshot of a single in-flight background shell run.
61///
62/// Produced by [`super::ShellExecutor::background_runs_snapshot`] and consumed
63/// by the TUI resources panel and the metrics snapshot update path.
64#[derive(Debug, Clone)]
65pub struct BackgroundRunSnapshot {
66    /// Opaque run identifier, encoded as a 32-character lowercase hex string.
67    pub run_id: String,
68    /// Original command string, already stored on the handle.
69    pub command: String,
70    /// Wall-clock milliseconds since spawn.
71    pub elapsed_ms: u64,
72}
73
74/// Final result delivered when a background run finishes.
75///
76/// Sent via `ToolEvent::Completed { run_id: Some(..), .. }` and buffered in
77/// `LifecycleState::pending_background_completions` for injection into the next turn.
78#[derive(Debug, Clone)]
79pub struct BackgroundCompletion {
80    /// The run that produced this result.
81    pub run_id: RunId,
82    /// Shell exit code (`0` = success).
83    pub exit_code: i32,
84    /// Filtered and truncated output text.
85    pub output: String,
86    /// `true` when `exit_code == 0`.
87    pub success: bool,
88    /// Wall-clock elapsed milliseconds from spawn to completion.
89    pub elapsed_ms: u64,
90    /// Original command string.
91    pub command: String,
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use std::collections::HashSet;
98
99    #[test]
100    fn run_id_display_is_32_char_hex() {
101        let id = RunId::new();
102        let s = id.to_string();
103        assert_eq!(s.len(), 32, "RunId should display as 32-char hex");
104        assert!(
105            s.chars().all(|c| c.is_ascii_hexdigit()),
106            "RunId should be lowercase hex, got: {s}"
107        );
108    }
109
110    #[test]
111    fn run_id_uniqueness() {
112        let ids: HashSet<String> = (0..100).map(|_| RunId::new().to_string()).collect();
113        assert_eq!(ids.len(), 100, "100 RunIds must all be distinct");
114    }
115
116    #[test]
117    fn run_id_copy_semantics() {
118        let a = RunId::new();
119        let b = a; // Copy, not move
120        assert_eq!(a, b);
121    }
122}