1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//! What the pane prints once a managed `claude` exits (#6766, relocated #8233).
//!
//! Why: #2023 component D appended `; echo '<hint>'` to every managed launch
//! command, so a `claude` that REFUSED the launch and quit before drawing a
//! frame printed the same "run `tm` to relaunch this session" line as a session
//! the operator worked in for an hour. #6766 replaced that with a three-way
//! branch on the exit status and the elapsed time.
//!
//! That branch used to be a `if`/`elif`/`$(( ))` shell fragment appended to the
//! typed pane command, which cost ~330 bytes of the line whose length is exactly
//! what #8233 is about. The BEHAVIOUR is unchanged and the wording is
//! byte-identical; only the evaluator moved. `tm internal-spawn-disclaimed
//! --launch-spec` is the process that waits for `claude`, so it has the real
//! exit status and the real elapsed time — strictly better inputs than
//! `$?` and `$(date +%s)` arithmetic in a shell that may have swallowed the
//! clock stamp.
//!
//! Three outcomes, unchanged from #6766:
//!
//! * non-zero status → [`FAILED_HINT_HEAD`] + the status + [`FAILED_HINT_TAIL`]
//! * status 0 within [`LAUNCH_FLOOR_SECS`] → [`REFUSED_HINT`]
//! * anything else → [`RELAUNCH_HINT`]
//!
//! **Why elapsed time and not the refusal text.** The refusal that motivated
//! this exits 0 — status alone cannot see it. #6766 weighed matching Claude
//! Code's own wording and rejected it: that string is an undocumented UI surface
//! a reworded release would silently take the detection with it. Elapsed time
//! couples to nothing Anthropic owns. It costs a false REFUSED line for an
//! operator who starts `claude` and quits inside [`LAUNCH_FLOOR_SECS`], which is
//! a wrong sentence in a pane, not a wrong state anywhere.
//!
//! Test: `report_names_a_nonzero_status`, `report_calls_an_immediate_zero_exit_refused`,
//! `report_keeps_the_relaunch_hint_for_a_real_session`,
//! `report_treats_a_signal_death_as_a_failure` in this file's `tests` module.
use Duration;
/// The one-line hint printed after a `claude` that genuinely ran (#2023 D).
///
/// Why: the pane stays alive as a bare shell when the runtime exits, and a bare
/// `tm` run from inside it relaunches the same session in place (#2023 C) — but
/// neither is discoverable unless the pane says so.
/// What: a short, single-line string, unchanged from #6766.
/// Test: `report_keeps_the_relaunch_hint_for_a_real_session`.
pub const RELAUNCH_HINT: &str = "tm: run `tm` to relaunch this session";
/// Printed when `claude` exited 0 but faster than a session can start (#6766).
///
/// Why: this is the refusal case — Claude Code declined the launch, said why on
/// its own last line, and exited successfully. The operator needs the pane to
/// name that as a failed relaunch and point at the message immediately above.
/// Test: `report_calls_an_immediate_zero_exit_refused`.
pub const REFUSED_HINT: &str = "tm: claude exited immediately without starting a session — \
this launch was refused (its own message is above); \
run `tm` to retry";
/// Leading half of the non-zero-status line; the status sits between this and
/// [`FAILED_HINT_TAIL`] (#6766).
///
/// Test: `report_names_a_nonzero_status`.
pub const FAILED_HINT_HEAD: &str = "tm: claude exited with status";
/// Trailing half of the non-zero-status line (see [`FAILED_HINT_HEAD`]).
pub const FAILED_HINT_TAIL: &str = "— the session did not start; run `tm` to retry";
/// Seconds under which a status-0 exit reads as a refusal, not a finished
/// session (#6766).
///
/// Why: Claude Code's background-session refusal prints and exits well inside a
/// second; a session an operator actually worked in lasts minutes. The value is
/// high enough to absorb a slow cold start that fails, low enough that the only
/// false positive is an operator who quits `claude` immediately.
/// Test: both `report_calls_an_immediate_zero_exit_refused` and
/// `report_keeps_the_relaunch_hint_for_a_real_session` straddle it.
pub const LAUNCH_FLOOR_SECS: u64 = 5;
/// Report how a managed `claude` exited, as the one line the pane prints.
///
/// Why: see this module's header — a refused relaunch must not read as a
/// finished session.
/// What: `code` is the child's exit code (`None` when a signal killed it, which
/// is a failure like any other non-zero status and is reported as 127, the value
/// a shell reports for the same case). `elapsed` is how long the child ran.
/// Returns the exact line, with no trailing newline.
/// Test: the four `report_*` tests below.