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
    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