Problem
wait_for_text is the wrong shape for many agentic terminal workflows. It answers a narrow question: did an exact future pattern appear before timeout? In real dev loops, the agent often needs to inspect output, fuzzy-match it, notice a different port, watch several panes, or recover from a wrong assumption.
The current failure modes are tracked across the wait-family issues:
The common root is that wait_for_text hides terminal output behind found plus matched_lines. If the agent can see the actual pane delta quickly, it can apply its own judgment with full project context.
Proposal: capture_since
Add a non-blocking, cursor-based pane observation tool that returns output rather than deciding whether output is good enough.
async def capture_since(
pane_id: str | None = None,
cursor: str | None = None,
session_name: str | None = None,
session_id: str | None = None,
window_id: str | None = None,
socket_name: str | None = None,
max_lines: int = 200,
max_bytes: int = 20000,
) -> CaptureSinceResult: ...
class CaptureSinceResult(BaseModel):
pane_id: str
cursor: str
lines: list[str]
elapsed_seconds: float
lines_missed: bool
truncated: bool
The exact field names can follow local model conventions, but the contract should stay simple: return bounded new pane text, a cursor to pass back next time, and structured metadata when output may have been lost or truncated.
Design decisions
| Decision |
Choice |
Why |
| Blocking model |
Return immediately |
Avoids agent lockout and lets the agent interleave panes. |
| First call |
Return visible screen and establish a cursor |
Useful immediately; avoids a wasted baseline-only call. |
| Cursor format |
Opaque versioned string |
Keeps the API stable if internals move from tmux grid coordinates to another backend later. |
| Judgment |
Agent judges, not ctx.sample() |
No hidden LLM call, no client sampling dependency, full project context stays with the agent. |
| Output size |
Bound with max_lines and max_bytes |
Prevents token blowups in log-heavy panes. |
The opaque cursor should encode enough server-side facts to detect stale cursors and history loss, likely including pane identity, history_size, cursor_y, pane dimensions or a content hash, and a cursor version. Callers must treat it as opaque.
Expected workflows
Start a server and inspect what happened:
send_keys("python server.py", pane_id="%5")
result = capture_since(pane_id="%5")
# Agent sees: Listening on 0.0.0.0:3333
Monitor multiple panes without blocking on one:
api = capture_since(pane_id="%1")
web = capture_since(pane_id="%2")
worker = capture_since(pane_id="%3")
Poll with caller-controlled backoff:
first = capture_since(pane_id="%5")
second = capture_since(pane_id="%5", cursor=first.cursor)
Upstream mechanisms
The first implementation should use the same tmux grid facts already used by wait_for_text: history_size, cursor_y, and capture-pane ranges. tmux capture coordinates are evaluated against live history in cmd-capture-pane.c, and history trimming happens in grid_collect_history(), so loss detection is still best-effort near history-limit.
Future backends should remain possible behind the opaque cursor. tmux has event-oriented surfaces in pipe-pane and control-mode output via control_write_output(), but those are separate streaming designs and should not block the initial snapshot/delta tool.
FastMCP ctx.sample() is intentionally out of scope for this tool. The agent already has the project context and can inspect the returned output directly. FastMCP sampling is client-capability-dependent and would hide cost and reasoning inside the tool.
Relationship to existing tools
| Tool |
Question it answers |
Blocking |
Best for |
wait_for_channel |
Has my authored command finished? |
Blocking, zero-poll in tmux |
Commands the agent sends. |
capture_since |
What happened since last inspection? |
Non-blocking |
Tailing, fuzzy judgment, multi-pane monitoring. |
wait_for_text |
Did this exact future pattern appear? |
Blocking, polling |
Known third-party output. |
wait_for_content_change |
Did the pane content change at all? |
Blocking, polling |
Tripwire before inspection. |
snapshot_pane |
What is the full pane state now? |
Non-blocking |
Full context and metadata. |
capture_since complements rather than replaces these tools. It removes the need to abuse boolean waits for exploratory observation.
Acceptance criteria
- New read-only MCP tool registered and documented as
capture_since.
- First call with
cursor=None returns bounded visible screen content and a cursor.
- Subsequent calls with the returned cursor return only new output where tmux history still permits it.
- Result includes structured
lines_missed and truncated metadata.
- Invalid, stale, or wrong-pane cursors fail clearly or return conservative metadata; do not silently pretend output is complete.
- Tests cover first-call visible content, delta-only follow-up, max-line/max-byte truncation, history-loss behavior, pane respawn/death behavior, and multi-pane calls.
- Docs route tailing/diagnosis workflows to
capture_since, authored command completion to wait_for_channel, and exact future matches to wait_for_text.
Problem
wait_for_textis the wrong shape for many agentic terminal workflows. It answers a narrow question: did an exact future pattern appear before timeout? In real dev loops, the agent often needs to inspect output, fuzzy-match it, notice a different port, watch several panes, or recover from a wrong assumption.The current failure modes are tracked across the wait-family issues:
wait_for_text's delays (90 seconds) is can lock agentic flows - blocking #59: blocking lockout when the pattern is wrong or output is unexpected.wait_for_textpoll tick.printf, spinners, and carriage-return redraws can sit on the baseline row.The common root is that
wait_for_texthides terminal output behindfoundplusmatched_lines. If the agent can see the actual pane delta quickly, it can apply its own judgment with full project context.Proposal:
capture_sinceAdd a non-blocking, cursor-based pane observation tool that returns output rather than deciding whether output is good enough.
The exact field names can follow local model conventions, but the contract should stay simple: return bounded new pane text, a cursor to pass back next time, and structured metadata when output may have been lost or truncated.
Design decisions
ctx.sample()max_linesandmax_bytesThe opaque cursor should encode enough server-side facts to detect stale cursors and history loss, likely including pane identity,
history_size,cursor_y, pane dimensions or a content hash, and a cursor version. Callers must treat it as opaque.Expected workflows
Start a server and inspect what happened:
Monitor multiple panes without blocking on one:
Poll with caller-controlled backoff:
Upstream mechanisms
The first implementation should use the same tmux grid facts already used by
wait_for_text:history_size,cursor_y, andcapture-paneranges. tmux capture coordinates are evaluated against live history incmd-capture-pane.c, and history trimming happens ingrid_collect_history(), so loss detection is still best-effort nearhistory-limit.Future backends should remain possible behind the opaque cursor. tmux has event-oriented surfaces in
pipe-paneand control-mode output viacontrol_write_output(), but those are separate streaming designs and should not block the initial snapshot/delta tool.FastMCP
ctx.sample()is intentionally out of scope for this tool. The agent already has the project context and can inspect the returned output directly. FastMCP sampling is client-capability-dependent and would hide cost and reasoning inside the tool.Relationship to existing tools
wait_for_channelcapture_sincewait_for_textwait_for_content_changesnapshot_panecapture_sincecomplements rather than replaces these tools. It removes the need to abuse boolean waits for exploratory observation.Acceptance criteria
capture_since.cursor=Nonereturns bounded visible screen content and a cursor.lines_missedandtruncatedmetadata.capture_since, authored command completion towait_for_channel, and exact future matches towait_for_text.