fix(capture): --plain no longer answers with a fraction of the pane

`capture` offers "two independent choices": how much (`--scrollback`) and
in what form (`--plain`). They were not independent. Every `--plain` grid
was built with `Config::default()`, whose 10,000 scrolling lines were
justified in a comment as "the daemon ring's order of magnitude".

The ring is capped in *bytes* — 8 MiB — which at ordinary line lengths is
nearer 100,000 lines. So the plain form silently dropped most of what the
raw form returned, and `--scrollback` could not bring it back. On a pane
that had printed 120,000 lines:

    capture           8,388,609 bytes, from line 30,764   (the whole ring)
    capture --plain     932,651 bytes, from line 109,973
    capture --scrollback --plain   identical to the above

Nine tenths of the pane missing, with nothing said. `--plain` is the form
the orchestration docs reach for (`tty7 wait %3 && tty7 capture %3
--plain`), so an agent reading the end of a long build log got the tail
and no reason to doubt it had the rest.

The grid is now sized from the segment it has to replay. Counting bytes
per column is not enough and the difference is the bug in miniature: a
line shorter than the pane is wide still costs a whole row, so dividing by
the width under-counts exactly when lines are short, which is most output
— that estimate alone recovered sixty thousand lines and still stopped
twenty thousand short. One row per newline plus one per screenful of wrap
is the bound that holds. Both forms now begin at line 30,764.

Over-estimating is free: alacritty grows its history as lines arrive
rather than allocating up front, and the daemon's ring bounds the whole
thing. Measured: an ordinary capture is unchanged at 8 MB resident and
0.3s; a full 8 MiB ring costs 343 MB for the 2.5s it takes.

The regression test uses short lines for the reason above, and fails
against the old fixed 10k grid.
This commit is contained in:
l0ng-ai
2026-08-22 23:51:47 +08:00
parent 449d38e538
commit e7d04066d2
2 changed files with 104 additions and 6 deletions
+6
View File
@@ -177,6 +177,12 @@ the new prompt alone — everything above the banner needs `--scrollback`.
decoded as UTF-8 (invalid bytes become U+FFFD). With `--plain`, those bytes
replayed through a terminal grid and printed as the text they produced.
Both forms cover the same span: the grid `--plain` replays through is sized to
hold whatever the segment can fill, so it never answers with less of the pane
than the raw form does. Rendering a full ring costs proportionate memory for
the moment it takes — a few hundred MB for the 8 MiB maximum, and nothing worth
measuring for an ordinary capture.
Either way it is a snapshot, not a stream: it collects the replay, settles for
~300 ms, and returns. Call it again for a newer one.
JSON: `{"pane","text"}`.