fix(desktop): recover remote terminal after laptop sleep/wake

Merged
SA
5mo ago
+139-1

Fixes #5130.

Problem

A desktop workspace terminal on a remote host freezes after the laptop sleeps and wakes — no output, keystrokes do nothing, and the only fix is to kill the pane and open a new one. The PTY itself keeps running in pty-daemon on the host; the break is client-side.

On suspend the terminal WebSocket goes half-open: the TCP connection is dead but readyState stays OPEN and no close event ever fires. terminal-ws-transport.ts only reconnects from its close handler (scheduleReconnect() is reached from a WS construction failure and the close handler, nowhere else), so it sat wedged on the dead pipe reporting connectionState === "open".

The web workspace terminal (apps/web/.../WebTerminal/TerminalConnection.ts) already recovers on resume via visibilitychange/resume/pageshow/online + a backoff reset in handleResume. The desktop transport never got the equivalent — and DOM resume events alone aren't enough on desktop, since app-suspend doesn't reliably fire visibilitychange/focus when the window was focused both before and after sleep. The dependable signal is a wall-clock gap.

Fix

Add a liveness/wake path to the transport, set up in connect() and torn down in disconnect() / disposeTransport():

  1. Wall-clock-gap watchdog — a 5s setInterval compares Date.now() between ticks; a gap > 20s means the process was suspended, so it force-recycles the (presumed half-open) socket. This catches the dead-but-OPEN case.
  2. online / focus / visibilitychange listeners — reset backoff and reconnect a dead socket immediately (a healthy/connecting one is left alone), mirroring TerminalConnection.handleResume on web.

reconnectNow() drops the dead socket without waiting for a close and re-dials. Re-attach is cheap: the host keeps the PTY alive, so reconnecting just re-subscribes and the protocol replays anything missed. The existing transport.socket !== socket guard means a late OS-delivered close on the old socket is harmless.

Testing

  • Folded the issue's sleep/wake repro into terminal-ws-transport.test.ts. It drives the real watchdog setInterval + Date.now()-gap path: fails on main (Expected: 2, Received: 1 — only one socket ever created) and passes with the fix.
  • bun test (3 pass), bun run lint (clean), bun run typecheck (pass).

Manual verification (real half-open socket) is a lid-close test: open a remote-host terminal running top, close the lid ≥1 min, reopen → recovers within ~5s.


Open in Stage

Summary by cubic

Recover remote desktop terminals after laptop sleep/wake by detecting suspension and force-reconnecting the WebSocket. Fixes frozen panes caused by half-open sockets where output and keystrokes stopped.

  • Bug Fixes
    • Added a wall-clock watchdog (checks every 5s); a >20s gap triggers a socket recycle.
    • Added resume listeners (online, focus, visibilitychange) to reset backoff and reconnect only if the socket is dead.
    • Introduced reconnectNow() and wired liveness setup/teardown in connect() and disconnect()/disposeTransport(); added a test that simulates half-open recovery.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved terminal websocket resilience when machines wake from sleep or experience network changes. The transport now detects stale connections and automatically establishes a fresh websocket to restore terminal connectivity.
  • Tests

    • Added test cases validating proper websocket recovery following machine sleep and network resume events.