diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc2e8a5..3c25cea8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -166,6 +166,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 this account's open-file limit is the ceiling on how many panes can run at once; raise it (`ulimit -n`, …)", keeping the original error in parentheses. +- **`doctor` now says when the server is running panes nothing holds.** An + interrupted `tty7 run` leaves one behind, and a window reconciling its layout + against concurrent edits leaves more — each a live shell holding a pty and + its descriptors. They were findable (`tty7 pane ls --all`, the switcher) and + recoverable (`tty7 pane close --orphans`), but nothing volunteered that they + were there: doctor's status row counted them among the panes without saying + any were stray. It is a row and a `server.orphans` count, not an exit code — + one stray after an interrupted `run` is ordinary, and failing on it would cry + wolf. + - **`doctor` now checks the configured shell.** A `shell` naming something that is not there — missing, a directory, not executable — makes every new tab and every `tty7 new` fail, while `doctor` reported `config ok`, because diff --git a/crates/tty7-cli/src/commands.rs b/crates/tty7-cli/src/commands.rs index 2a79a3df..7b7a6e75 100644 --- a/crates/tty7-cli/src/commands.rs +++ b/crates/tty7-cli/src/commands.rs @@ -2000,13 +2000,51 @@ fn doctor(ctx: &Context, backend: &mut dyn Backend) -> Result { // an id the reader has no reason to doubt, which is the state // `doctor` is run in. Only checked when the server answered — // there is nothing to check against otherwise. + // Shells the server runs that no workspace holds. An interrupted + // `tty7 run` leaves one; a window reconciling its layout against + // concurrent edits leaves more — measured at 17 from 160 + // operations, every one a live shell holding a pty and its + // descriptors. They are recoverable and findable, but nothing + // volunteered that they were there: the `status` row counts them + // among the panes without saying any are stray. + // + // A row, not an exit code. One orphan after an interrupted `run` + // is ordinary and documented as such, and `tty7 doctor || alert` + // firing on it would cry wolf. What was missing is the sentence. + // + // Counted here because the tree is already in hand for the row + // below, and because it takes a server to have panes at all. + let mut strays = 0usize; if let Ok(machine) = fetch_machine(backend) { dangling = Some(note_dangling_context(&mut rows, ctx, &machine)); + if let Ok(running) = backend.list_panes() { + let held: std::collections::HashSet = machine + .workspaces + .iter() + .flat_map(|ws| ws.tabs.iter()) + .flat_map(|tab| tab.root.pane_ids()) + .collect(); + strays = running + .iter() + .filter(|info| !held.contains(&info.pane_id)) + .count(); + } + if strays > 0 { + rows.push(vec![ + "stray panes".to_string(), + format!( + "{} running that no workspace holds — `tty7 pane ls --all` \ + names them, `tty7 pane close --orphans` ends them", + panes_count(strays) + ), + ]); + } } server = json!({ "reachable": true, "dialect_ok": dialect_ok, "build": hello.build, + "orphans": strays, "status": serde_json::to_value(&status)?, "routes": serde_json::to_value(&routes)?, }); diff --git a/docs/cli/reference.mdx b/docs/cli/reference.mdx index d12f0c5a..94528dd4 100644 --- a/docs/cli/reference.mdx +++ b/docs/cli/reference.mdx @@ -320,7 +320,7 @@ nothing, so `tty7 agents` shows it standing still and `tty7 wait` sits there until it times out. Outdated hooks fail the same quiet way. Hooks are a local install, so under `-m` the row reads `unknown`. -JSON: `{"context":{"config_dir","workspace","pane"},"server":{"reachable","dialect_ok","build","status","routes"},"config":{"ok","state","dir_writable","shell_problem"},"hooks":{"installed","outdated","not_installed"}}` +JSON: `{"context":{"config_dir","workspace","pane"},"server":{"reachable","dialect_ok","build","orphans","status","routes"},"config":{"ok","state","dir_writable","shell_problem"},"hooks":{"installed","outdated","not_installed"}}` — the context fields are booleans, not values, and each `hooks` field is a list of agent slugs. `context` also carries `workspace_gone` and `pane_gone` when a server answered and could be asked; both are absent when none did, so that "it @@ -329,7 +329,11 @@ false only when the file failed to parse and tty7 is running on defaults; `config.state` is the same sentence the table prints. `config.dir_writable` is probed by writing, not read off the mode bits, because a read-only mount or an ACL leaves a `0700` directory that refuses every write — false means settings -cannot be saved and `tty7 new` cannot file a workspace. `config.shell_problem` is null unless the +cannot be saved and `tty7 new` cannot file a workspace. `server.orphans` counts the panes the +server is running that no workspace holds — an interrupted `run` leaves one, +and each is a live shell holding a pty. It is a row and a number, not an exit +code: one stray is ordinary, and `tty7 pane close --orphans` ends them. +`config.shell_problem` is null unless the configured `shell` names something that cannot be launched — missing, a directory, not executable — in which case every new tab and every `tty7 new` fails; a shell given as a bare name is left to `PATH` and never reported here,