From 449d38e5385d632ebac37c5edc6a07ec4b0ae688 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sat, 22 Aug 2026 23:41:08 +0800 Subject: [PATCH] docs(wait): say that an unknown pane answers `exit`, and pin it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `tty7 wait` is the one address-taking verb that does not refuse a pane the server has no record of. `capture`, `procs`, `send` and `pane close` all exit 1 on the same address; `wait` answers `exit`, `matched: true`, `stale: true`, and exits 0. That is the right behaviour and must not change. The server forgets a pane once it is reaped, so "the worker finished and was cleaned up" and "that id never existed" are one question to it — measured, not assumed: a pane that really ran and exited comes back byte-identical to `%9999`, and `pane ls --all` has forgotten both. Refusing would break the first case, which is the ordinary end of an orchestration: you wait on work that may already be over. What was missing is that nobody had written it down. Neither the CLI reference nor the orchestration page said what an unknown pane does, and both define `exit` as "the pane is gone" — true of a typo, but not what a reader takes from it when every neighbouring verb errors. An orchestrator that trusts a bare `wait` as proof the work happened gets an instant success from a stale id and reads an empty capture as "no output". So both pages say it, and a test pins it. Without the test this is an accident that reads like a bug, and the obvious "fix" — make it error like its siblings — would silently break waiting on finished work. --- crates/tty7-cli/src/commands.rs | 38 +++++++++++++++++++++++++++++++++ docs/agents/orchestration.mdx | 8 +++++++ docs/cli/reference.mdx | 11 ++++++++++ 3 files changed, 57 insertions(+) diff --git a/crates/tty7-cli/src/commands.rs b/crates/tty7-cli/src/commands.rs index 69cbff19..9b24329c 100644 --- a/crates/tty7-cli/src/commands.rs +++ b/crates/tty7-cli/src/commands.rs @@ -3778,6 +3778,44 @@ mod tests { assert_eq!(backend.control_calls, vec![ControlRequest::AgentStates]); } + /// A pane the server has no record of answers `exit`, and that is the + /// decision rather than an oversight. + /// + /// `wait` is the one address-taking verb that does not refuse an unknown + /// pane — `capture`, `procs`, `send` and `pane close` all exit 1 on the + /// same address. It cannot join them: the server keeps no record of a pane + /// once it is reaped, so "finished and was cleaned up" and "never existed" + /// are the same question to it, and refusing would break the first. That + /// one is the ordinary end of an orchestration — you wait on work that may + /// already be over. + /// + /// `stale` is what a caller reads to tell the two apart from a *watched* + /// finish: false only when the pane moved into the state while the wait + /// was running. An `exit` that comes back stale means the pane was gone + /// before anyone looked, whatever the reason. + #[test] + fn wait_on_a_pane_the_server_never_had_reports_it_gone() { + let mut backend = mock(); + backend.replies.push_back(ReplyOk::AgentStates(Vec::new())); + let out = run_cli( + &["tty7", "wait", "%9999", "--until", "exit"], + &Context::default(), + &mut backend, + ); + let json = json_of(out); + assert_eq!(json["pane"], 9999); + assert_eq!(json["status"], "exit", "gone is gone, known or not"); + assert_eq!( + json["matched"], true, + "and it ends the wait rather than spinning on a ghost" + ); + assert_eq!( + json["stale"], true, + "nobody watched this happen — the only signal separating a pane \ + that finished under the wait from one that was already absent" + ); + } + /// `--changed` is the fix for a level-triggered status: right after a /// `send`, the agent still reports last turn's state. The flag refuses the /// position the wait arrived at and wakes only once the pane moves. diff --git a/docs/agents/orchestration.mdx b/docs/agents/orchestration.mdx index 7c830378..ed0d2b5d 100644 --- a/docs/agents/orchestration.mdx +++ b/docs/agents/orchestration.mdx @@ -66,6 +66,14 @@ about the pane, because not everything worth waiting on is an agent: | `free` | The foreground command has exited; the pane is back to its bare shell | | `exit` | The pane is gone. Ends every wait, whether you asked for it or not | +`exit` also answers for a pane id the server has never held. Once a pane is +reaped the server keeps no record of it, so "your worker finished and was +cleaned up" and "that id is wrong" arrive identically — `exit` with +`stale: true`, and exit status `0`. Unlike `capture` or `send`, a mistyped +address will not fail here. If a worker's result matters, read it back +(`tty7 capture`) rather than treating a bare `wait` as proof the work +happened. + ### Waiting on a command instead of an agent An agent says when it is done. A `cargo test` does not — so for a plain pane the diff --git a/docs/cli/reference.mdx b/docs/cli/reference.mdx index d7770be5..864a9156 100644 --- a/docs/cli/reference.mdx +++ b/docs/cli/reference.mdx @@ -235,6 +235,17 @@ want directly after a `send`; a command quick enough to finish inside one The reply carries the agent's message and native session id. The JSON's `stale` flag says whether the answer might belong to the previous turn. +`wait` is the one address-taking verb that does **not** refuse a pane the +server has no record of — it answers `exit`, `matched: true`, `stale: true` and +exits `0`, where `capture` and `send` would exit `1`. It cannot do otherwise: +the server forgets a pane once it is reaped, so a pane that finished and was +cleaned up and a pane id that never existed are the same question to it, and +refusing would break the first — which is the ordinary end of an +orchestration. `stale: false` is the only thing that says a pane moved into its +state while you were watching; a stale `exit` means it was already gone, +whatever the reason. Check the id itself with `pane ls --all` if you need to +tell a typo from a finished worker. + JSON: `{"pane","status","matched","stale","activity","message","session_id"}`. A timeout exits `124` with the same object plus `"timed_out": true` — `matched` is `false` there, and `stale` still says whether the pane moved