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}