docs(wait): say that an unknown pane answers exit, and pin it

`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.
This commit is contained in:
l0ng-ai
2026-08-22 23:41:08 +08:00
parent ec7a7a2fa5
commit 449d38e538
3 changed files with 57 additions and 0 deletions
+38
View File
@@ -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.
+8
View File
@@ -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
+11
View File
@@ -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