Skip to main content

lean_ctx/shell/
tee_policy.rs

1//! Single source of truth for the shell "tee" decision — whether the full,
2//! pre-compression output is saved to a recovery file so the agent can retrieve
3//! it instead of re-running the command.
4//!
5//! Both the CLI buffered path (`shell::exec`) and the MCP `ctx_shell` handler
6//! call [`should_tee`], so `TeeMode::Failures` means the exact same thing on
7//! both: a non-zero exit code — never a brittle substring match on the word
8//! "error" (which misses `fatal:`, `permission denied`, localized messages, and
9//! terse failures). See #809 / #811.
10
11use crate::core::config::TeeMode;
12
13/// Decide whether to persist a recovery copy of shell output.
14pub fn should_tee(
15    mode: &TeeMode,
16    exit_code: i32,
17    blank_output: bool,
18    content_elided: bool,
19    original_tokens: usize,
20    compressed_tokens: usize,
21) -> bool {
22    if blank_output {
23        return false;
24    }
25    match mode {
26        TeeMode::Never => false,
27        // Explicit retention mode remains independent of compression.
28        TeeMode::Always => true,
29        TeeMode::Failures => exit_code != 0 && content_elided,
30        TeeMode::HighCompression => {
31            content_elided
32                && (exit_code != 0
33                    || (original_tokens > 100
34                        && savings_pct(original_tokens, compressed_tokens) > 70.0))
35        }
36    }
37}
38
39/// True only when the inline representation does not contain the complete
40/// non-blank output. Wrappers such as `<error>…</error>` do not count as
41/// elision.
42pub fn output_was_elided(full_output: &str, inline_output: &str) -> bool {
43    let full_output = full_output.trim_end_matches(['\r', '\n']);
44    !full_output.is_empty() && !inline_output.contains(full_output)
45}
46
47/// Percentage of tokens removed by compression, clamped to `0.0` when the
48/// original was empty. Shared so CLI and MCP report identical savings.
49pub fn savings_pct(original_tokens: usize, compressed_tokens: usize) -> f64 {
50    if original_tokens == 0 {
51        return 0.0;
52    }
53    (original_tokens.saturating_sub(compressed_tokens) as f64 / original_tokens as f64) * 100.0
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn never_mode_never_tees() {
62        assert!(!should_tee(&TeeMode::Never, 1, false, true, 1000, 10));
63        assert!(!should_tee(&TeeMode::Never, 0, false, true, 1000, 10));
64    }
65
66    #[test]
67    fn always_mode_tees_non_blank_only() {
68        assert!(should_tee(&TeeMode::Always, 0, false, false, 100, 50));
69        assert!(!should_tee(&TeeMode::Always, 0, true, true, 100, 50));
70    }
71
72    #[test]
73    fn failures_tee_only_when_output_was_elided() {
74        assert!(should_tee(&TeeMode::Failures, 1, false, true, 5, 2));
75        assert!(!should_tee(&TeeMode::Failures, 1, false, false, 5, 5));
76        assert!(!should_tee(&TeeMode::Failures, 0, false, true, 9999, 10));
77        assert!(!should_tee(&TeeMode::Failures, 1, true, true, 0, 0));
78    }
79
80    #[test]
81    fn high_compression_requires_actual_elision() {
82        assert!(should_tee(
83            &TeeMode::HighCompression,
84            0,
85            false,
86            true,
87            1000,
88            100
89        ));
90        assert!(!should_tee(
91            &TeeMode::HighCompression,
92            0,
93            false,
94            false,
95            1000,
96            100
97        ));
98        assert!(!should_tee(
99            &TeeMode::HighCompression,
100            0,
101            false,
102            true,
103            1000,
104            900
105        ));
106        assert!(!should_tee(
107            &TeeMode::HighCompression,
108            1,
109            false,
110            false,
111            5,
112            5
113        ));
114    }
115
116    #[test]
117    fn detects_full_output_inside_error_wrapper() {
118        assert!(!output_was_elided("hi\n", "<error>hi\n[exit:1]</error>"));
119        assert!(output_was_elided(
120            "first\nmissing\nlast\n",
121            "first\n…\nlast"
122        ));
123        assert!(!output_was_elided("", ""));
124    }
125
126    #[test]
127    fn default_tee_mode_is_high_compression() {
128        assert_eq!(TeeMode::default(), TeeMode::HighCompression);
129    }
130
131    #[test]
132    fn savings_pct_handles_zero_original() {
133        assert_eq!(savings_pct(0, 0), 0.0);
134        assert!((savings_pct(1000, 100) - 90.0).abs() < f64::EPSILON);
135    }
136}