Skip to content

feat: capture_since — non-blocking, cursor-based delta capture for agentic flows #60

Description

@tony

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions