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:
-
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.
-
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.
-
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.
-
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.
Problem. libtmux consumers that anchor on
display-message + capture-panefor "wait for new output" semantics — e.g. libtmux-mcp'swait_for_text— cannot reliably detect thatgrid_collect_historytrim has fired during continuous output. tmux trims (~10% ofhistory-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 wherehsizedips below entry once entry was sub-cap.Verified gap in tmux's exposed surface (referenced at tmux@134ba6c):
grid.cgrid_collect_history— decrementsgd->hsizebyhlimit/10when at cap; rows are freed immediatelygrid-view.cgrid_view_scroll_region_up— calls collect then scroll in the same call, so the dip is sub-tickcmd-capture-pane.ccmd_capture_pane_history—-S npositive evaluates against livehsizeat capture time, so absolute-index math drifts after a trimformat.c— only exposes#{history_size},#{history_bytes},#{history_limit},#{history_all_bytes}; none monotonic across trimsoptions-table.c— 68 hooks total; none fire on history trim (nohistory-trimmed, nogrid-collected)grid_linecarries atimefield but it isn't exposed via format stringsInvestigation directions for libtmux:
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.Per-line content sentinel option. Capture row 0 contents at entry; compare each poll; if the row-0 bytes differ, trim happened (or
clear-historyran). Costs one extracapture-pane -S cy0 -E cy0per tick.Upstream feature request to tmux. A monotonic
#{history_lines_evicted}format string, or ahistory-trimmedhook, would let consumers detect the event without polling row-0 content. This is the cleanest fix but needs upstream agreement.Document the limitation in
Pane.capture_panedocs 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_textwith an honest "best-effort nearhistory-limit" contract plus a runtimectx.warningin the trim-risk band, and steers agents towait_for_channel(composedtmux 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.