feat(pane_tools): Add capture_since incremental pane reader#63
Merged
Conversation
why: Agents need an observation-first pane reader that can follow terminal output across turns without repeatedly returning the same scrollback. what: - Add cursor-based capture_since with lifecycle, history-loss, and truncation metadata - Register the readonly tool and route prompts, docs, and server guidance to it for repeated observation - Cover delta capture, retained-history anchors, cursor validation, lifecycle invalidation, truncation, discovery, and MCP smoke behavior
why: The new pane reader needs docs that distinguish first-use cursors, retained-history deltas, and history-loss recovery from pagination. what: - Explain capture_since cursor lifecycle and lines_missed semantics - Separate protocol pagination, search paging, and observation cursors - Refresh tool overview, troubleshooting, and role demo references
why: tmux history-limit trimming can rebase pane grid rows without a sampled history_size shrink, causing capture_since to report incomplete output as an exact delta. what: - Re-anchor risky capture_since cursors by unique row fingerprint - Fall back to lines_missed when history-limit trimming makes the cursor ambiguous - Add a regression for history-limit output loss
Member
Author
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #63 +/- ##
==========================================
- Coverage 85.98% 84.31% -1.68%
==========================================
Files 40 42 +2
Lines 2448 2684 +236
Branches 319 359 +40
==========================================
+ Hits 2105 2263 +158
- Misses 260 317 +57
- Partials 83 104 +21 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…ardless of pane height why: _cursor_anchor_lost's pane_height guard returned False when clear-history zeroed history_size but a subsequent resize grew the pane, masking the complete wipe and letting capture_since report incomplete output as an exact delta. what: - Short-circuit anchor-lost detection when history_size drops to 0 from a positive baseline — grid reset always destroys the anchor - Add regression test combining clear-history with pane resize
…ross tmux versions why: tmux 3.6 retains enough of the original prompt row that _find_unique_cursor_match re-anchors on the surviving hash after a 120-line flood past history-limit=20, causing the test to report lines_missed=False on CI. what: - Prefill scrollback before first capture so cursor has history_size>0 - Clear history after the flood to guarantee the anchor hash is gone - Update assertion marker to match the new post-clear echo
…ing notes why: the multi-line docstring documenting the tmux 3.7+ retroactive history-limit quirk, caching invariant, and separation rationale was stripped to a one-liner during the state.py extraction.
why: the display-message field format (separator, types, literal flag values) was dropped during the state.py extraction — this is load-bearing for anyone editing the format string in _read_pane_state.
…state module why: _raise_if_pane_lifecycle_changed moved to the shared state.py module but kept "during wait" in its error messages — misleading for capture_since callers that never issued a wait. what: - Drop "during wait" from error messages, use generic phrasing - Update test match patterns to use short matches
Member
Author
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
…t contract why: the comment claimed output text was byte-identical to a prior monolith, but instruction strings have evolved since the original refactor — the actual invariant is the join shape, not frozen content.
…read_delta why: the docstring documents that history-limit is fixed at pane creation and safe to cache for the lifetime of a capture — calling it inside the 3-attempt stability loop contradicted that contract and incurred redundant subprocess round-trips on retry.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
capture_since— a non-blocking, cursor-based incremental pane reader that returns only new terminal output since the previous observationcapture_sincefor repeated-observation workflows (tailing, diagnosis, multi-pane monitoring)_PaneStateand lifecycle helpers intopane_tools/state.pysowait_for_textandcapture_sinceshare the same tmux state readingChanges by area
Tool implementation
tools/pane_tools/capture_since.py: Core tool — cursor encode/decode, stable double-read with retry, delta extraction, fingerprint-based re-anchoring under trim risk, tail-preserving line/byte truncationtools/pane_tools/state.py: Extracted_PaneStateNamedTuple and_read_pane_state()helper (previously inline inwait.py)models.py:CaptureSinceResultPydantic model with cursor, lines, and structured loss/truncation metadataServer registration
server.py: Registercapture_sincein response-limited tools list; update instruction strings to recommend it for repeated observationDocumentation
docs/tools/pane/capture-since.md: Dedicated tool page explaining cursor lifecycle, delta semantics, andlines_missedrecoverydocs/topics/pagination.md: Distinguish protocol pagination, search paging, and observation cursorsdocs/topics/prompting.md,docs/recipes.md,docs/demo.md: Route tailing and diagnosis workflows tocapture_sinceDesign decisions
capture-since-v1:prefix + base64 JSON payload keeps the wire format stable if internals change; agents must treat the cursor as opaquehistory-limittrimming makes the positional anchor ambiguous, re-locate via content hashes; reportlines_missed=Trueif the fingerprint is non-unique or absentctx.sample(): No hidden LLM call — the agent has full project context and can interpret terminal output directlyVerification
Verify
capture_sinceis registered and response-limited:Verify shared state helpers are imported by both tools:
Test plan
test_capture_since_first_call_returns_visible_screen_and_cursor— baseline capture returns content and opaque cursortest_capture_since_followup_returns_only_new_output— delta contains only rows written after the cursortest_capture_since_follows_anchor_into_retained_history— cursor tracks output that scrolls into retained scrollbacktest_capture_since_marks_lines_missed_after_history_limit_trim—lines_missed=Truewhen trim exceeds fingerprint recoverytest_capture_since_marks_lines_missed_after_history_clear—lines_missed=Trueafterclear-historytest_capture_since_reports_same_row_rewrite— carriage-return/printf overwrites are detectedtest_capture_since_truncates_with_structured_metadata—max_lines/max_bytesproduce correcttruncated_lines/truncated_bytestest_capture_since_rejects_malformed_cursor— garbage and wrong-version cursors raiseToolErrortest_capture_since_rejects_cursor_for_different_pane— cross-pane cursor reuse raisesToolErrortest_capture_since_rejects_respawned_pane_cursor— PID mismatch afterrespawn-paneraisesToolErrortest_capture_since_rejects_dead_pane_cursor— dead pane raisesToolErrortest_capture_since_does_not_block_event_loop— async wrapper offloads to threadruff check,ruff format --check,mypy --strict,pytestCloses #60