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    // TODO(review): expose via TUI panel for per-run elapsed display.
47    #[allow(dead_code)]
48    pub started_at: Instant,
49    /// Cancellation token. Cancel to request graceful shutdown.
50    pub abort: CancellationToken,
51    /// OS process ID, if known. Reserved for future SIGTERM escalation on shutdown.
52    // TODO(review): use once safe signal-sending wrapper (e.g. nix crate) is available.
53    #[allow(dead_code)]
54    pub child_pid: Option<u32>,
55}
56
57/// Final result delivered when a background run finishes.
58///
59/// Sent via `ToolEvent::Completed { run_id: Some(..), .. }` and buffered in
60/// `LifecycleState::pending_background_completions` for injection into the next turn.
61#[derive(Debug, Clone)]
62pub struct BackgroundCompletion {
63    /// The run that produced this result.
64    pub run_id: RunId,
65    /// Shell exit code (`0` = success).
66    pub exit_code: i32,
67    /// Filtered and truncated output text.
68    pub output: String,
69    /// `true` when `exit_code == 0`.
70    pub success: bool,
71    /// Wall-clock elapsed milliseconds from spawn to completion.
72    pub elapsed_ms: u64,
73    /// Original command string.
74    pub command: String,
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use std::collections::HashSet;
81
82    #[test]
83    fn run_id_display_is_32_char_hex() {
84        let id = RunId::new();
85        let s = id.to_string();
86        assert_eq!(s.len(), 32, "RunId should display as 32-char hex");
87        assert!(
88            s.chars().all(|c| c.is_ascii_hexdigit()),
89            "RunId should be lowercase hex, got: {s}"
90        );
91    }
92
93    #[test]
94    fn run_id_uniqueness() {
95        let ids: HashSet<String> = (0..100).map(|_| RunId::new().to_string()).collect();
96        assert_eq!(ids.len(), 100, "100 RunIds must all be distinct");
97    }
98
99    #[test]
100    fn run_id_copy_semantics() {
101        let a = RunId::new();
102        let b = a; // Copy, not move
103        assert_eq!(a, b);
104    }
105}