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