diff --git a/crates/tty7-core/src/daemon/pane.rs b/crates/tty7-core/src/daemon/pane.rs index 9454e2bc..be8c8369 100644 --- a/crates/tty7-core/src/daemon/pane.rs +++ b/crates/tty7-core/src/daemon/pane.rs @@ -756,6 +756,13 @@ impl Drop for SigtermUnblocked { } } +/// The exit code to report, or `None` when the pane did not leave one. +/// +/// A signal death is `None` rather than `128 + signal`: the shell convention +/// is the *shell's*, and inventing it here would hand a caller a number the +/// command never returned. `tty7 run` turns `None` into an exit 1 that says +/// so — on stderr, and as `exit_code_known: false` — which is what lets an +/// orchestrator tell a command the OOM killer took from one that exited 1. fn reported_exit_code(status: &portable_pty::ExitStatus) -> Option { match status.to_string().starts_with("Terminated by") { true => None, @@ -4328,6 +4335,32 @@ mod tests { assert!(hex_val(b'/').is_none()); } + /// A signal is not an exit code, and must not be dressed up as one. + /// + /// `run` reports `exit_code_known: false` off the back of this `None`, and + /// the reference tells orchestrators to branch on that flag — it said the + /// opposite for a while, so this is here to keep the two agreeing. + #[test] + fn a_pane_killed_by_a_signal_reports_no_exit_code() { + assert_eq!( + reported_exit_code(&portable_pty::ExitStatus::with_exit_code(0)), + Some(0) + ); + assert_eq!( + reported_exit_code(&portable_pty::ExitStatus::with_exit_code(42)), + Some(42) + ); + + // How portable_pty spells a signal death, and the only thing that + // separates one from an ordinary code here. + let signalled = portable_pty::ExitStatus::with_signal("SIGKILL"); + assert!( + signalled.to_string().starts_with("Terminated by"), + "the spelling this turns on changed: {signalled}" + ); + assert_eq!(reported_exit_code(&signalled), None); + } + #[test] fn osc133_exit_code_parsing() { let mut s = OscSniffer::new(); diff --git a/docs/cli/reference.mdx b/docs/cli/reference.mdx index 2089b69e..06a9837e 100644 --- a/docs/cli/reference.mdx +++ b/docs/cli/reference.mdx @@ -18,7 +18,7 @@ Accepted anywhere on the line, before or after the subcommand. | Flag | Effect | |---|---| | `-m, --machine ` | Route to a linked machine over the local server's existing link. Matches the full link key (`me@devbox:22`) or the bare host (`devbox`). SSH links only; a down link, or a jump/proxy chain, is refused with a reason rather than dialled fresh. | -| `--json` | One JSON object on stdout instead of the human table. A verb that *fails* prints its message on stderr and no JSON at all, so a reader parsing stdout must check the exit code first. Two verbs are exceptions, because there the bad news is the answer: `pane close` still prints `{"closed":[…],"failed":[…]}`, and `wait` still prints the state it gave up in. (`run` is not an exception — a child exiting nonzero is the verb succeeding, and its JSON is printed as usual.) | +| `--json` | One JSON object on stdout instead of the human table. A verb that *fails* prints its message on stderr and no JSON at all, so a reader parsing stdout must check the exit code first. Three verbs are exceptions, because there the bad news is the answer: `pane close` still prints `{"closed":[…],"failed":[…]}`, `wait` still prints the state it gave up in, and `doctor` still prints its whole report — an unreachable server is the finding, and `tty7 doctor || alert` needs both the exit code and the rows. (`run` is not an exception — a child exiting nonzero is the verb succeeding, and its JSON is printed as usual.) | | `-q, --quiet` | No output on success. Errors still go to stderr. | ## Environment @@ -94,10 +94,12 @@ for the server to read in the short window it waits. `run` then exits 1 as a stand-in and says so on stderr, and this flag is how a caller tells that 1 from a command that really did exit 1. -A command **killed by a signal** also reports `exit: 1` — not the `128+N` a -shell would give — but with `exit_code_known: true`, because a status was read. -So `run` cannot distinguish a command the OOM killer took from one that exited -1 on its own; read the pane's output if that difference matters. +A command **killed by a signal** reports `exit: 1` — not the `128+N` a shell +would give — with `exit_code_known: false` and the same stderr note as any +other unknown code, because a status that says "terminated by a signal" is not +an exit code the daemon will invent one from. That flag is exactly how a +caller tells a command the OOM killer took from one that exited 1 on its own, +so an orchestrator does not have to read the pane to find out. ### `tty7 new [PATH] [--open]`