Operating system
Windows
Netcatty version
1.1.71 (also reproduced with a diagnostic build based on current main)
How did you install Netcatty?
GitHub Release (.exe)
Affected area
SSH connection / terminal
Can you reproduce it?
Often (>50%) during longer TUI sessions
Steps to reproduce
- Connect to a Linux host over SSH in a large terminal (the captured session was 266x68).
- Run a full-screen TUI that uses DEC 2026 synchronized output and emits frequent repaint blocks. OpenCode 1.18.4 is a reliable reproducer, but no OpenCode plugin is involved.
- Start a streaming operation and let the Netcatty window become unfocused, occluded, or minimized while output continues.
- Return to the terminal. The visible TUI can remain permanently frozen even though the SSH session and remote process are still alive.
Expected behavior
Every renderer ingress byte should either reach xterm in order or remain attached to a scheduled drain. Losing browser animation frames, changing pane visibility, or applying flow control must not leave pending terminal output with no wake-up path.
Actual behavior
A live capture caught the permanent stall before xterm:
- The renderer continued logging
renderer-receive events. The final burst contained many 12-34 KB TUI chunks.
- There were no corresponding
renderer-write-done events after the stall began.
- The xterm private write buffer was empty (
_writeBuffer.length=0, _pendingData=0, _isSyncWriting=false), synchronized-output mode was false, and the sync render handler was not buffering.
- Renderer CPU was nearly idle, rather than busy parsing or painting.
- The remote TUI remained alive and busy, while both PTY queues were empty (
TIOCINQ=0, TIOCOUTQ=0). SSH/TCP was healthy.
- Kitty keyboard handling was disabled at runtime (
useKitty=false).
- Bringing the window to the foreground caused newly requested animation frames to run, but did not drain the already-stranded terminal batch.
This places the loss between renderer ingress and term.write(), in Netcatty's write coalescer / visibility scheduling path rather than SSH, the remote PTY, xterm parsing, or Kitty keyboard handling.
Two scheduling edges appear capable of stranding pending data:
createWriteCoalescer relies on requestAnimationFrame for alternate-screen TUI batches without a wall-clock deadline. Chromium/Electron may suppress rAF while a window is occluded.
- The visible idle flush is implemented as a debounce: every incoming chunk clears and rearms the timer. A sustained stream can postpone the only timer-based drain indefinitely. If flow control then pauses ingress, there may be no later push to create a new wake-up edge.
- If a scheduled callback is rejected by
shouldFlushScheduledFrame(), its scheduled state is cleared before returning while pending bytes remain. A later visibility transition needs an explicit rearm/flush path.
Suggested direction
A correctness fix should preserve all bytes, ordering, and ingress ACK accounting:
- Give rAF scheduling a bounded wall-clock fallback.
- Treat the idle flush timer as a maximum-latency deadline, not a reset-on-every-chunk debounce.
- If the flush gate rejects a scheduled callback, retain or rearm a drain instead of leaving pending bytes unscheduled.
- Force-drain the coalescer and serial write queue on focus/visibility resume.
- Add regression tests for a missing rAF callback, a gate transition while a frame is pending, and continuous sub-deadline chunks.
A local diagnostic build that bypassed the extra coalescer for SSH completed 217/217 writes with zero outstanding batches while the window was minimized. That confirms the transport and xterm path can keep up, but bypassing the normal SSH pipeline is too broad for an upstream fix because it skips timestamps, prompt formatting, output-pressure slicing, and other terminal behavior.
Related work
Logs / screenshots
Representative terminal performance sequence at the stall:
renderer-receive ingressBytes=34564
renderer-receive ingressBytes=22111
renderer-receive ingressBytes=29620
renderer-receive ingressBytes=24925
renderer-receive ingressBytes=28726
... no renderer-write-done after this point ...
Targeted tests for the proposed scheduling guards pass locally, including a simulated rAF callback that never arrives.
Before submitting
Operating system
Windows
Netcatty version
1.1.71 (also reproduced with a diagnostic build based on current
main)How did you install Netcatty?
GitHub Release (
.exe)Affected area
SSH connection / terminal
Can you reproduce it?
Often (>50%) during longer TUI sessions
Steps to reproduce
Expected behavior
Every renderer ingress byte should either reach xterm in order or remain attached to a scheduled drain. Losing browser animation frames, changing pane visibility, or applying flow control must not leave pending terminal output with no wake-up path.
Actual behavior
A live capture caught the permanent stall before xterm:
renderer-receiveevents. The final burst contained many 12-34 KB TUI chunks.renderer-write-doneevents after the stall began._writeBuffer.length=0,_pendingData=0,_isSyncWriting=false), synchronized-output mode was false, and the sync render handler was not buffering.TIOCINQ=0,TIOCOUTQ=0). SSH/TCP was healthy.useKitty=false).This places the loss between renderer ingress and
term.write(), in Netcatty's write coalescer / visibility scheduling path rather than SSH, the remote PTY, xterm parsing, or Kitty keyboard handling.Two scheduling edges appear capable of stranding pending data:
createWriteCoalescerrelies onrequestAnimationFramefor alternate-screen TUI batches without a wall-clock deadline. Chromium/Electron may suppress rAF while a window is occluded.shouldFlushScheduledFrame(), its scheduled state is cleared before returning while pending bytes remain. A later visibility transition needs an explicit rearm/flush path.Suggested direction
A correctness fix should preserve all bytes, ordering, and ingress ACK accounting:
A local diagnostic build that bypassed the extra coalescer for SSH completed 217/217 writes with zero outstanding batches while the window was minimized. That confirms the transport and xterm path can keep up, but bypassing the normal SSH pipeline is too broad for an upstream fix because it skips timestamps, prompt formatting, output-pressure slicing, and other terminal behavior.
Related work
Logs / screenshots
Representative terminal performance sequence at the stall:
Targeted tests for the proposed scheduling guards pass locally, including a simulated rAF callback that never arrives.
Before submitting