agents_wait: wait for idle, dialog or text instead of polling snapshots (cx-ncc.93)
Summary
An agent that waits for another agent in cmux had to poll agents_snapshot. It can now call agents_wait (MCP) or cmux agents wait (CLI), which share one code path (cli/agents/wait.rs) and answer when the condition holds:
until: idle: the agent of aterm_,tab_oragent_id, or an agent chat's session, stops working (idle, done, or blocked on a dialog;match.statesays which).until: dialog: a permission or question dialog is pending, a target's when given (theagents_dialog_listread).until: text: literal text, or a regex withregex: true, appears on a terminal's screen.
timeout_seconds defaults to 30 and is at most 120. The receipt is {until, matched, waited_seconds, match, state}, where state is a fresh agents_snapshot-shaped snapshot cut by the #20609 receipt rule. A timeout is matched: false, not an error; a condition that already holds answers at once. Bad arguments, an invalid regex (checked with the regex crate, the daemon's own engine and limits) and an unknown target are refused as not_run before any wait. For until: dialog, a term_, tab_ or agent_ target that no owner knows is refused like idle and text; a target of another kind is taken as a dialog request id, which may name a dialog that has not appeared yet, so that wait runs to its timeout instead of being refused. A text target whose terminal closes mid-wait reports match.state: "closed", the same result idle gives.
cmux agents wait exits 1 on matched: false and still prints the JSON receipt, as screen wait does (docs/cli-contract.md and the cmux-workspace skill say so).
The MCP server answers one request at a time, so an agents_wait call holds later calls on the same connection for up to its timeout (at most 120 s). terminal_wait already behaves this way; this PR does not change it.
What wakes a wait:
textis event-driven: it is the daemon's ownterminal.wait, which wakes on terminal output.idleanddialoghold the daemon'ssession.eventsstream open and check on each event (at most every 200 ms). They also check every second, because an agent chat's state and the dialog feed live in the app, whoseevents.streampublishes no event for them. So dialogs and chat-only agent state are polled at 1 s; terminal agents (daemon agent records, OSC 7501 program status) wake on daemon events. With no daemon stream, the wait checks every second.
An MCP call with _meta.progressToken gets notifications/progress every 5 s (progress = seconds waited, total = timeout). The CLI shows the #20627 stderr notes for waits of 35 s or more (human output, or JSON with a TTY), with a new agents_waiting_progress string (English and Japanese).
Security: the tool is read-only (readOnlyHint, idempotent, no key). It reads other agents' state and pending dialogs, as agents_snapshot and agents_dialog_list already do, and a terminal's screen, as terminal_wait already does; its description says so. Chat transcripts are not readable for text, because no existing read exposes them. No socket method or relay policy changed; RemoteRelayCommandPolicy stays deny.
plans/cmux-next/mcp.md lists the tool.
Proof
Behavior tests: eleven at the MCP boundary (nine originally, two added in review) (cli/mcp/tests/agent_wait.rs, through Server::handle/run with the fake owners, whose daemon snapshot and dialog feed change mid-wait) and one CLI test through a real headless daemon (tests/cli/agents_wait_cli.rs: text written to a terminal, an agent reported working then idle, a 1 s timeout, a usage error).
- Red, tests only at 33e0b44fc4af:
dispatch.sh <wt> agents_wait, run 38651317708. The nine MCP tests failed onunknown tool: agents_wait(behavior, not compile). - Green: run 38654134200 at 45491f13c7ae passed
test (linux); its Linux clippy flagged two lints, fixed in c9ee02af48bf; focused run 38656540524 on that head (c9ee02af48bf) passed every job, with all tenagents_waittests run and passing on Linux (macOS ran the same filter). - Full: run 38660253813 on the same head. Linux
cargo testpassed, including all tenagents_waittests. The run is red on failures outside this diff:- Windows build and Windows tests:
cli/wire/socket.rscallscrate::app_identity, which is#[cfg(unix)]. This is a base break from bc697beb0b57 (cx-is1kf.5), and it stops the Windows Rust tests before they run. - Linux TUI smoke (
smoke-tui.py:953): a workspace-order assertion afterprefix-W, in TUI code this PR doesn't touch. - macOS jobs: the runner refused the job (
Set up runner), so they never ran. The macOS clippy and focused filter passed in run 38656540524.
- Windows build and Windows tests:
- Local:
scripts/cmux-next/crash_ratchet.pyok;check-no-godfiles.sh --only rust --base origin/feat-cmux-nextpasses (the first version grewcli/app.rspast 1000 lines; it no longer does).
Review fixups (one red, one green, same focused command dispatch.sh <worktree> agents_wait):
- Red, tests only at 2c6c1867b8fb: run 38666204524. The two new tests failed on behavior:
agents_wait_refuses_an_invalid_regex_and_an_unknown_dialog_agent_before_waiting(an invalid regex was not refused) andagents_wait_text_reports_a_terminal_that_closes_mid_wait(a closed terminal was an error). The other nine passed. - Fix in 1268f9996b5e, then one base merge of feat-cmux-next at be93becd1f4c (includes #20767 and #20745) in 0f3cb113fdc9.
- Green on 0f3cb113fdc9: run 38685803291, every job passed (21 jobs, 0 failed); its Linux log shows all eleven MCP
agent_waittests and the real-daemon CLI testagents_wait_cli_answers_on_text_and_idle_through_a_real_daemonran and passed. - Local on the merged head:
crash_ratchet.pyok;check-no-godfiles.sh --only rust --base be93becd1f4cpasses.
Changelog
Added: agents_wait MCP tool and cmux agents wait, which wait until an agent goes idle, a dialog appears or text reaches a terminal, instead of polling snapshots.
🤖 Generated with Claude Code