Skip to content

Commit 3377573

Browse files
committed
mcp(docs[instructions]): Route pane exit waits and name next steps
why: An agent with a one-shot job in its own pane had no instruction pointing at wait_for_pane_exit, split_window did not mention the fan-out tool, and a wait_for_channel timeout gave no next step. what: - Name wait_for_pane_exit in the wait routing line, paid for by trimming the read-tools line; the 2048-byte budget tests still pass - Point split_window at split_window_many - Give wait_for_channel timeout and lost-server errors a suggestion
1 parent 05fccce commit 3377573

6 files changed

Lines changed: 44 additions & 5 deletions

File tree

‎CHANGES‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ scrollback of the new session's panes before a long job starts.
3232

3333
### Fixes
3434

35+
**Server instructions and wait errors name the right next tool**
36+
37+
The server instructions route a pane's own process to `wait_for_pane_exit`,
38+
`split_window` points to `split_window_many`, and a `wait_for_channel` timeout
39+
or lost server says what to call next.
40+
3541
**A socket tmux cannot open is an error, not an empty list**
3642

3743
`list_sessions`, `list_windows` and `list_panes` used to answer `[]` when tmux

‎src/libtmux_mcp/server.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,19 @@
9090

9191
_INSTR_READ_TOOLS = (
9292
"Prefer snapshot_pane over capture_pane + get_pane_info; capture_since "
93-
"for repeated observation/tailing; display_message for tmux variables."
93+
"for tailing; display_message for tmux variables."
9494
)
9595

9696
_INSTR_WAIT_NOT_POLL = (
97-
"WAIT, DON'T POLL: run_command for authored commands needing "
98-
"status; wait_for_channel for custom tmux wait-for; capture_since "
97+
"WAIT, DON'T POLL: run_command for authored commands (status); "
98+
"wait_for_pane_exit for shell= jobs; "
99+
"wait_for_channel for custom wait-for; capture_since "
99100
"for tailing; wait_for_text for output you don't author "
100101
"(patterns=null=any output; stop=[] bails); "
101102
"send_keys_batch for raw input."
102103
)
103104

105+
104106
#: Gap-explainer: write-hook tools are intentionally absent. See module
105107
#: comment above for when to add another ``_GAP`` segment vs. push the
106108
#: explanation into a tool description.

‎src/libtmux_mcp/tools/wait_for_tools.py‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,13 @@ async def wait_for_channel(
223223
f"wait-for timeout: channel {cname!r} was not signalled within "
224224
f"{effective_timeout}s"
225225
)
226-
raise ExpectedToolError(msg) from e
226+
raise ExpectedToolError(
227+
msg,
228+
suggestion=(
229+
"The channel is still usable. Check the command that should "
230+
"signal it with capture_since, then wait again or raise timeout."
231+
),
232+
) from e
227233
if returncode != 0:
228234
detail = stderr.decode(errors="replace").strip()
229235
msg = f"wait-for failed for channel {cname!r}: {detail or f'exit {returncode}'}"
@@ -253,7 +259,10 @@ async def wait_for_channel(
253259
"no longer running, so the channel was probably never signalled "
254260
"— tmux exits 0 for both. Re-check the work you were waiting on."
255261
)
256-
raise ExpectedToolError(msg)
262+
raise ExpectedToolError(
263+
msg,
264+
suggestion="Call get_server_info; create_session starts a new server.",
265+
)
257266
return f"Channel {cname!r} was signalled (timeout {effective_timeout}s)"
258267

259268

‎src/libtmux_mcp/tools/window_tools.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ def split_window(
172172
173173
Creates a new pane by splitting an existing one. Use direction to choose
174174
above/below/left/right. Returns the new pane's info including its pane_id.
175+
To add several panes at once under one layout, use split_window_many.
175176
176177
Parameters
177178
----------

‎tests/test_server.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ def test_base_instructions_prefer_typed_completion_over_polling() -> None:
413413
"""_BASE_INSTRUCTIONS names typed completion and observation primitives."""
414414
assert "run_command" in _BASE_INSTRUCTIONS
415415
assert "wait_for_channel" in _BASE_INSTRUCTIONS
416+
assert "wait_for_pane_exit" in _BASE_INSTRUCTIONS
416417
assert "capture_since" in _BASE_INSTRUCTIONS
417418
assert "wait_for_text" in _BASE_INSTRUCTIONS
418419
# The catch-all form replaced the separate wait_for_content_change

‎tests/test_wait_for_tools.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,3 +544,23 @@ async def _drive() -> str:
544544

545545
assert "was signalled" in result
546546
assert time.monotonic() - started < 1.9
547+
548+
549+
@pytest.mark.usefixtures("mcp_session")
550+
def test_wait_for_channel_timeout_error_carries_a_next_step(
551+
mcp_server: Server,
552+
) -> None:
553+
"""A timed-out wait tells the agent the channel is reusable and what to read."""
554+
from libtmux_mcp._utils import ExpectedToolError
555+
556+
with pytest.raises(ExpectedToolError) as excinfo:
557+
asyncio.run(
558+
wait_for_channel(
559+
channel="wf_hint_test",
560+
timeout=0.3,
561+
socket_name=mcp_server.socket_name,
562+
)
563+
)
564+
565+
assert "still usable" in (excinfo.value.suggestion or "")
566+
assert "capture_since" in (excinfo.value.suggestion or "")

0 commit comments

Comments
 (0)