Skip to content

[Bug] Terminal output can permanently stall before xterm when a coalescer flush wake-up is lost #2467

Description

@zopenb

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

  1. Connect to a Linux host over SSH in a large terminal (the captured session was 266x68).
  2. 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.
  3. Start a streaming operation and let the Netcatty window become unfocused, occluded, or minimized while output continues.
  4. 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:

  1. createWriteCoalescer relies on requestAnimationFrame for alternate-screen TUI batches without a wall-clock deadline. Chromium/Electron may suppress rAF while a window is occluded.
  2. 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.
  3. 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

  • I searched existing issues and did not find a duplicate
  • I removed passwords, private keys, host addresses, and other secrets from this report

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingready-for-agentFully specified, ready for an AFK agenttriageTouched by Cursor automationtriage:admittedReserved for serialized automatic issue triage admissiontriage:bug-readyConfirmed Netcatty bug ready for automatic implementation

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions