Skip to content

wrapped-pty: status band corrupts on scroll over high-latency transports (sprites) #260

Description

@hexsprite

Attaching to a sprites box renders correctly at first, then corrupts once content scrolls near the bottom: the status band is drawn twice at different rows, and band text ends up interleaved into a content line mid-screen.

Not a sizing bug

Ruled out by measurement on a live box:

  • terminal size propagates: host 143 cols produced tmux pane=143x46 (one row for the band)
  • SIGWINCH propagates: host resized 30x100 to 55x150, remote stty size reported 30x100 then 55x150
  • the transport does not split the stream across stdout/stderr: stderr carried 0 bytes in TTY mode

What is actually happening

wrapped-pty/run.ts:969 restricts the OUTER terminal to rows 1..innerRows so the inner program cannot scroll into the footer, and re-asserts it on relayout/resize (:460, :789, :1095).

But tmux inside the box emits its own ESC[N;Mr constantly. Captured from a real attach — 14 seconds, tmux running seq 1 500:

total bytes                       = 4601
ESC[r    (region reset)           = 0
ESC[N;Mr (region set)             = 57
ESC[?1049 (alt screen)            = 1

57 scroll-region writes in 14s, all landing on the same outer terminal as the wrapper band.

That is normally harmless: the inner PTY is sized to innerRows, so tmux computes regions inside that height and never addresses the band row. The problem is the resize window. When the alert band appears or disappears, reservedRows() changes, the wrapper moves its band and resizes the inner pty — and the remote repaints at the OLD size until the resize lands. On docker that window is microseconds. Over a network transport it is a round trip, and tmux paints into rows that are now band rows.

Hence: fine until something scrolls or the band changes, then band text stranded in content and a doubled band.

Scope

The wrapper assumption — that an inner resize takes effect before the next paint — is shared, so hetzner/daytona/remote-docker are exposed too; sprites just has enough latency to hit it reliably. Reproduces with agentbox claude --provider sprites in a repo whose agent output scrolls.

Possible directions

  • Repaint/clear the reserved rows after a relayout rather than only redrawing the band, so a stale remote paint is overwritten rather than left behind.
  • Defer the band move until the inner pty has produced output at the new size.
  • Re-assert the scroll region on a short timer while a resize is in flight.

Found while adding the sprites provider. The provider-side sizing (SDK PTY bridge with spawn({tty,cols,rows}) + resize()) is fixed and verified; this is the remaining wrapper-side piece.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions