Skip to content

Investigate libtmux-side trim detection for scrollback consumers #671

@tony

Description

@tony

Problem. libtmux consumers that anchor on display-message + capture-pane for "wait for new output" semantics — e.g. libtmux-mcp's wait_for_text — cannot reliably detect that grid_collect_history trim has fired during continuous output. tmux trims (~10% of history-limit) then immediately scrolls new lines back, so sampled #{history_size} stays clamped near the cap and never appears below the anchor value. A 2 ms high-frequency probe across ~3000 samples confirmed there is no sub-poll window where hsize dips below entry once entry was sub-cap.

Verified gap in tmux's exposed surface (referenced at tmux@134ba6c):

  • grid.c grid_collect_history — decrements gd->hsize by hlimit/10 when at cap; rows are freed immediately
  • grid-view.c grid_view_scroll_region_up — calls collect then scroll in the same call, so the dip is sub-tick
  • cmd-capture-pane.c cmd_capture_pane_history-S n positive evaluates against live hsize at capture time, so absolute-index math drifts after a trim
  • format.c — only exposes #{history_size}, #{history_bytes}, #{history_limit}, #{history_all_bytes}; none monotonic across trims
  • options-table.c — 68 hooks total; none fire on history trim (no history-trimmed, no grid-collected)
  • grid_line carries a time field but it isn't exposed via format strings

Investigation directions for libtmux:

  1. Wrap a delta-detection helper. A Pane.observe_grid(callable) primitive that captures row-0 content + (hsize, cursor_y) at entry, polls the same per tick, and returns "an eviction definitely happened since entry" when row-0 content changes. This is what every wait-style consumer is reinventing.

  2. Per-line content sentinel option. Capture row 0 contents at entry; compare each poll; if the row-0 bytes differ, trim happened (or clear-history ran). Costs one extra capture-pane -S cy0 -E cy0 per tick.

  3. Upstream feature request to tmux. A monotonic #{history_lines_evicted} format string, or a history-trimmed hook, would let consumers detect the event without polling row-0 content. This is the cleanest fix but needs upstream agreement.

  4. Document the limitation in Pane.capture_pane docs so downstream consumers know the limit before they roll their own polling layer.

Why this lands here, not on tmux/tmux directly. libtmux is the consumer-facing API; tmux's grid model is what it is. If libtmux can offer a shared helper, every downstream tool (libtmux-mcp, tmuxinator-style harnesses, test scaffolding, agent flows) benefits at once. An upstream tmux feature would still want a libtmux wrapper.

Downstream context. libtmux-mcp ships wait_for_text with an honest "best-effort near history-limit" contract plus a runtime ctx.warning in the trim-risk band, and steers agents to wait_for_channel (composed tmux wait-for -S) for deterministic command completion. The PR landing that work documents this same gap. A libtmux-level helper would let multiple downstream tools share the cost.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions