Review follow-up on the salvaged #47772 work. Three defects made the
filter a no-op or actively harmful in production, plus both Copilot
review items.
1. The blank-line burst filter never fired. pty_bridge.py spawns via
ptyprocess.PtyProcess.spawn() and never calls setraw(), so the PTY
line discipline runs with ONLCR: every LF the child writes reaches
xterm as CRLF. The /\n{50,}/ pattern requires consecutive LF, so a
real 1000-row burst matched nothing (verified against a live PTY:
b"A"+b"\n"*5+b"B" is read back as b"A\r\n\r\n\r\n\r\n\r\nB").
Now matches /(?:\r?\n){50,}/.
2. Bursts split across WebSocket frames were not collapsed. bridge.read()
does os.read(fd, 65536) per drain tick and each read is forwarded as
its own frame, so a burst spans frames and each fragment fell under
the 50 threshold (3000 rows survived in a 40-byte-read simulation).
The sanitizer now holds back a trailing newline run — including a lone
trailing CR, since a frame can split a CRLF pair — and resolves it on
the next frame or on flush.
3. Erase-code stripping was permanent, not resume-scoped. resumeParam is
the durable session identity and is never cleared after connect, so
every spinner/progress/status redraw in a resumed session lost its
ESC[K and left stale glyphs. Suppression is now bounded to
PTY_RESUME_SANITIZE_WINDOW_MS (30s) after connect; burst collapsing
still applies for the life of the socket.
Copilot review items:
- flush() no longer writes a buffered partial CSI into xterm. #pending
only ever holds an incomplete sequence, and emitting one leaves the
parser in an in-escape state that swallows output after reconnect. A
buffered newline run is still emitted (collapsed).
- Test expectations updated accordingly.
Tests: 29 cases (was 18), now using CRLF fixtures that match real PTY
output, plus cross-frame burst reassembly, CRLF-pair frame splits, and
post-window erase preservation. Full web suite 135 passing.