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}