Skip to content

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

Merged
saddlepaddle merged 1 commit into
mainfrom
fix/desktop-terminal-sleep-wake-recovery
Jun 5, 2026
Merged

saddlepaddle merged 1 commit into
mainfrom
fix/desktop-terminal-sleep-wake-recovery

Conversation

@saddlepaddle

@saddlepaddle saddlepaddle commented Jun 5, 2026 •

Copy link
Copy Markdown
Collaborator

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.

Written for commit e352f3d. Summary will update on new commits.

Review in cubic

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.

The terminal WebSocket goes half-open on suspend: the TCP connection is
dead but readyState stays OPEN and no close event fires. The transport
only reconnects from its close handler, so it sat wedged on the dead pipe
reporting connectionState "open" and the pane stayed frozen until killed.

Add a liveness/wake path to the transport, set up in connect() and torn
down in disconnect()/disposeTransport():
- A wall-clock-gap watchdog (setInterval) that recycles a presumed
  half-open socket when a tick gap shows the process was suspended. This
  is the dependable desktop signal; app-suspend doesn't reliably fire
  focus/visibility.
- online/focus/visibilitychange listeners that reset backoff and
  reconnect a dead socket immediately, mirroring the web TerminalConnection.

Re-attach is cheap: the host keeps the PTY alive, so reconnecting just
re-subscribes and replays.

Closes #5130
@ghost

ghost commented Jun 5, 2026

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 3 individual chapters for you:

Title
1 Define liveness tracking state in TerminalTransport
2 Wire liveness lifecycle to transport connection
3 Test terminal recovery after machine sleep
Open in Stage

Chapters generated by Stage for commit e352f3d on Jun 5, 2026 3:05am UTC.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2491ffbc-718f-48a1-aad9-85b55775add8

📥 Commits

Reviewing files that changed from the base of the PR and between 2386f52 and e352f3d.

📒 Files selected for processing (2)
  • apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.test.ts
  • apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.ts

📝 Walkthrough

Walkthrough

TerminalTransport gains a wall-clock watchdog and DOM resume event listeners to detect and recover from half-open WebSockets after laptop sleep/wake. The watchdog measures gaps between timer ticks; a large gap indicates suspension and triggers immediate reconnect. DOM events reset backoff and reconnect if the socket is unhealthy. Setup and teardown are wired into connect, disconnect, and disposeTransport. Tests validate the sleep/wake recovery with system-time mocking.

Changes

Sleep/Wake Detection and Terminal Reconnection

Layer / File(s) Summary
Liveness state contract
apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.ts
TerminalTransport interface gains _livenessTimer, _lastLivenessTick, and _resumeListener fields to track the watchdog interval, last tick time, and DOM event handler.
Liveness monitoring implementation
apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.ts
New constants and functions: reconnectNow() forces socket close and schedules immediate reconnect; handleResume() resets backoff and reconnects if socket is unhealthy; setupLiveness() starts the watchdog interval (comparing wall-clock gaps against a suspend threshold) and attaches online, focus, and visibilitychange listeners; teardownLiveness() stops the interval and removes listeners.
Connection lifecycle integration
apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.ts
connect() calls setupLiveness() to enable watchdog and DOM listeners; disconnect() and disposeTransport() both call teardownLiveness() to stop the watchdog and remove listeners during cleanup.
Test coverage for sleep/wake recovery
apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.test.ts
Test imports expanded to include jest and setSystemTime; afterEach cleanup now resets mocked system time and reverts to real timers. New test simulates sleep/wake by opening a transport, halting the WebSocket (half-open scenario), advancing system time by two minutes, and asserting a second socket is created, confirming recovery from the suspended state.

Sequence Diagram

sequenceDiagram
  participant App
  participant WatchdogInterval as Watchdog<br/>Interval
  participant DOMEvents as DOM Events<br/>(online, focus, etc.)
  participant TransportLogic as TerminalTransport
  participant WebSocket

  App->>TransportLogic: connect()
  TransportLogic->>TransportLogic: setupLiveness()
  TransportLogic->>WatchdogInterval: start interval (check wall-clock gap)
  TransportLogic->>DOMEvents: attach listeners
  
  Note over WatchdogInterval: Normal operation: gap < threshold
  WatchdogInterval->>TransportLogic: tick (no action)
  
  Note over App: Laptop sleeps → socket half-open, no close event
  Note over WatchdogInterval: Gap > SUSPEND_THRESHOLD detected
  WatchdogInterval->>TransportLogic: reconnectNow()
  TransportLogic->>WebSocket: close dead socket
  TransportLogic->>TransportLogic: scheduleReconnect()
  
  Note over App: OR: DOM resume event fires
  DOMEvents->>TransportLogic: handleResume()
  TransportLogic->>TransportLogic: check if socket is dead
  alt Socket not OPEN/CONNECTING
    TransportLogic->>TransportLogic: reconnectNow()
  end
  
  App->>TransportLogic: disconnect()
  TransportLogic->>TransportLogic: teardownLiveness()
  TransportLogic->>WatchdogInterval: stop interval
  TransportLogic->>DOMEvents: remove listeners
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • superset-sh/superset#4685: Both PRs implement websocket recovery on page resume/background by adding lifecycle-driven resume/visibility/online listeners and triggering immediate reconnect when the socket is no longer alive.
  • superset-sh/superset#4566: Both PRs modify apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.ts's TerminalTransport connection and reconnect behavior in connect() and transport state management.

Poem

🐰 A laptop sleeps, the socket lies half-dead,
no close event to wake the thread,
but now a watchdog ticks the clock,
detects the gap, and breaks the lock—
reconnect flows, and the PTY's back,
resuming life along its track! 🌱

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing remote terminal recovery after laptop sleep/wake.
Description check ✅ Passed The PR description is comprehensive and follows the template structure with sections for problem, fix, testing, and related issues (closes #5130).
Linked Issues check ✅ Passed The code changes fully address issue #5130: wall-clock watchdog detects half-open sockets, reconnectNow() drops dead sockets, resume listeners reset backoff, and tests verify the socket is recycled on wake.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the half-open socket issue: liveness tracking, resume listeners, and the test reproducing the sleep/wake scenario.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/desktop-terminal-sleep-wake-recovery

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a sleep/wake recovery mechanism to the desktop remote terminal WebSocket transport, fixing a half-open socket problem where the TCP connection dies silently (no close event) after a laptop sleeps and wakes. The solution adds a wall-clock-gap watchdog (setInterval + Date.now() comparison) to detect suspension, plus online/focus/visibilitychange listeners to mirror the existing web terminal's handleResume path.

  • Watchdog timer (_livenessTimer): fires every 5s, compares wall-clock time; a gap > 20s signals sleep, triggering a forced reconnect (reconnectNow) that drops the dead socket without waiting for a close event.
  • DOM resume listeners: reset backoff and reconnect only when the socket is confirmed dead; setup/teardown is tracked via _resumeListener and wired into connect()/disconnect()/disposeTransport().
  • Tests: a new bun test drives the fake-timer + fake-clock path and confirms a second socket is created after the gap is detected.

Confidence Score: 3/5

The core watchdog is sound but handleResume can race with the async pre-flight on remote /hosts/ workspaces, producing an orphaned socket on every sleep/wake cycle.

The wall-clock-gap watchdog and teardown wiring are solid. handleResume guards against a live socket by inspecting readyState but not connectionState. For /hosts/ remote URLs the watchdog triggers connect() which starts an async pre-flight before creating a WebSocket — during that window transport.socket is null. If online fires, handleResume sees a null socket, skips the early-return, and calls reconnectNow a second time. Both pre-flights race to openSocket, both pass the currentUrl/connectionState guards, and two WebSockets are opened to the same terminal. The first is silently overwritten without being closed, leaking a connection. The test covers only ws://host/terminal/t1 (non-hosts, synchronous open) so this path is not caught.

terminal-ws-transport.ts — specifically handleResume and its interaction with the pre-flight path in connect() for /hosts/ remote URLs.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.ts Adds liveness watchdog and DOM resume listeners; handleResume lacks a guard for connectionState === connecting, which can create a duplicate WebSocket for remote /hosts/ URLs when an online/focus event fires while a pre-flight HTTP request is still in flight.
apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.test.ts Adds a sleep/wake recovery test using fake timers and fake system time; covers the wall-clock gap path cleanly. Teardown properly restores real timers and system time.

Sequence Diagram

sequenceDiagram
    participant WD as Watchdog (setInterval)
    participant RL as ResumeListeners (online/focus/vis)
    participant RT as reconnectNow()
    participant HR as handleResume()
    participant T as Transport
    participant WS as WebSocket

    Note over T,WS: Normal connected state (socket OPEN)
    Note over WD: Laptop sleeps - JS paused, socket goes half-open
    Note over WD: Laptop wakes - JS resumes
    WD->>WD: "tick fires: gap > 20s"
    WD->>RT: reconnectNow()
    RT->>T: "socket = null"
    RT->>WS: dead.close() best-effort
    RT->>T: setConnectionState closed
    RT->>T: connect() pre-flight for /hosts/ URLs async
    T->>T: "connectionState = connecting, socket = null"
    RL->>HR: online event fires on wake
    HR->>HR: "socket = transport.socket null"
    HR->>HR: null check fails, falls through
    HR->>RT: reconnectNow() again - duplicate race
    Note over T,WS: Two pre-flights complete, two WebSockets opened, first orphaned
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.ts:208-216
`handleResume` doesn't guard against `connectionState === "connecting"`, so it calls `reconnectNow` even while a pre-flight HTTP request is already in progress. For remote `/hosts/` URLs, the waketime sequence is: watchdog fires → `reconnectNow` → `connect()` starts async `primeRelayAffinity` → socket is still `null`, state is `"connecting"` → `online` event fires → `handleResume` sees `null` socket → skips the early-return → calls `reconnectNow` again. Both pre-flights resolve and each calls `openSocket()`, passing the `currentUrl`/`connectionState` guards, creating two simultaneous WebSocket connections. The first socket is overwritten in `transport.socket` without being closed, leaving it orphaned on both the client and server.

```suggestion
	const socket = transport.socket;
	if (
		socket &&
		(socket.readyState === WebSocket.OPEN ||
			socket.readyState === WebSocket.CONNECTING)
	) {
		return;
	}
	// Also skip if a connection attempt is already in progress (e.g., a
	// pre-flight HTTP request for /hosts/ URLs where transport.socket is null
	// while connectionState is still "connecting"). Without this guard, an
	// `online` event firing during the watchdog's pre-flight would call
	// reconnectNow() a second time, producing two concurrent openSocket()
	// closures and an orphaned socket.
	if (transport.connectionState === "connecting") return;
	reconnectNow(transport);
```

Reviews (1): Last reviewed commit: "fix(desktop): recover remote terminal af..." | Re-trigger Greptile

Comment on lines +208 to +216
const socket = transport.socket;
if (
socket &&
(socket.readyState === WebSocket.OPEN ||
socket.readyState === WebSocket.CONNECTING)
) {
return;
}
reconnectNow(transport);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 handleResume doesn't guard against connectionState === "connecting", so it calls reconnectNow even while a pre-flight HTTP request is already in progress. For remote /hosts/ URLs, the waketime sequence is: watchdog fires → reconnectNow → connect() starts async primeRelayAffinity → socket is still null, state is "connecting" → online event fires → handleResume sees null socket → skips the early-return → calls reconnectNow again. Both pre-flights resolve and each calls openSocket(), passing the currentUrl/connectionState guards, creating two simultaneous WebSocket connections. The first socket is overwritten in transport.socket without being closed, leaving it orphaned on both the client and server.

Suggested change
const socket = transport.socket;
if (
socket &&
(socket.readyState === WebSocket.OPEN ||
socket.readyState === WebSocket.CONNECTING)
) {
return;
}
reconnectNow(transport);
const socket = transport.socket;
if (
socket &&
(socket.readyState === WebSocket.OPEN ||
socket.readyState === WebSocket.CONNECTING)
) {
return;
}
// Also skip if a connection attempt is already in progress (e.g., a
// pre-flight HTTP request for /hosts/ URLs where transport.socket is null
// while connectionState is still "connecting"). Without this guard, an
// `online` event firing during the watchdog's pre-flight would call
// reconnectNow() a second time, producing two concurrent openSocket()
// closures and an orphaned socket.
if (transport.connectionState === "connecting") return;
reconnectNow(transport);
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.ts
Line: 208-216

Comment:
`handleResume` doesn't guard against `connectionState === "connecting"`, so it calls `reconnectNow` even while a pre-flight HTTP request is already in progress. For remote `/hosts/` URLs, the waketime sequence is: watchdog fires → `reconnectNow` → `connect()` starts async `primeRelayAffinity` → socket is still `null`, state is `"connecting"` → `online` event fires → `handleResume` sees `null` socket → skips the early-return → calls `reconnectNow` again. Both pre-flights resolve and each calls `openSocket()`, passing the `currentUrl`/`connectionState` guards, creating two simultaneous WebSocket connections. The first socket is overwritten in `transport.socket` without being closed, leaving it orphaned on both the client and server.

```suggestion
	const socket = transport.socket;
	if (
		socket &&
		(socket.readyState === WebSocket.OPEN ||
			socket.readyState === WebSocket.CONNECTING)
	) {
		return;
	}
	// Also skip if a connection attempt is already in progress (e.g., a
	// pre-flight HTTP request for /hosts/ URLs where transport.socket is null
	// while connectionState is still "connecting"). Without this guard, an
	// `online` event firing during the watchdog's pre-flight would call
	// reconnectNow() a second time, producing two concurrent openSocket()
	// closures and an orphaned socket.
	if (transport.connectionState === "connecting") return;
	reconnectNow(transport);
```

How can I resolve this? If you propose a fix, please make it concise.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.ts">

<violation number="1" location="apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.ts:216">
P2: `handleResume` doesn't guard against `connectionState === "connecting"`, so it calls `reconnectNow` even when a pre-flight connection attempt is already in progress (socket is still `null` during async relay affinity). This creates a race where both the watchdog-initiated and the online-event-initiated `connect()` calls proceed to `openSocket()`, producing two simultaneous WebSocket connections with the first being overwritten without close — leaving an orphaned socket on client and server.

Add a guard before calling `reconnectNow`:
```ts
if (transport.connectionState === "connecting") return;
```</violation>
</file>
Architecture diagram
sequenceDiagram
    participant App as Desktop Terminal
    participant Transport as TerminalTransport
    participant WS as WebSocket
    participant Backend as pty-daemon
    participant OS as OS

    Note over App,Backend: NEW: Liveness detection for sleep/wake recovery

    App->>Transport: connect(terminal, url)
    Transport->>Transport: setupLiveness() (setInterval 5s, add listeners)
    Transport->>WS: new WebSocket(url)
    WS-->>Transport: onopen
    Transport->>Transport: setConnectionState("open")
    Transport->>Transport: attach socket listeners
    Note over Transport,Backend: Normal operation…

    Note over OS: Laptop sleeps – socket goes half‑open (no close event)
    Transport->>Transport: [setInterval tick] compare Date.now()
    alt gap > 20s (process was suspended)
        Transport->>Transport: reconnectNow()
        Transport->>WS: dead.close() (force close old socket)
        Transport->>WS: new WebSocket(url)
        WS-->>Transport: onopen
        Transport->>Backend: re‑subscribe
        Backend-->>Transport: replay (if any)
        Transport-->>App: terminal resumed
    else gap ≤ 20s (normal tick)
        Transport-->>Transport: nothing
    end

    Note over OS: Laptop wakes – resume events fire (online / focus / visibilitychange)
    opt Resume event received
        Transport->>Transport: handleResume()
        Transport->>Transport: reset backoff
        alt socket is dead (readyState not OPEN or CONNECTING)
            Transport->>Transport: reconnectNow()
            Transport->>WS: new WebSocket…
        else socket healthy
            Transport-->>Transport: no‑op (leave existing connection)
        end
    end

    Note over Transport: Cleanup on explicit disconnect or dispose
    App->>Transport: disconnect()
    Transport->>Transport: teardownLiveness() (clearInterval, remove listeners)
    Transport->>WS: close()
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

) {
return;
}
reconnectNow(transport);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: handleResume doesn't guard against connectionState === "connecting", so it calls reconnectNow even when a pre-flight connection attempt is already in progress (socket is still null during async relay affinity). This creates a race where both the watchdog-initiated and the online-event-initiated connect() calls proceed to openSocket(), producing two simultaneous WebSocket connections with the first being overwritten without close — leaving an orphaned socket on client and server.

Add a guard before calling reconnectNow:

if (transport.connectionState === "connecting") return;
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.ts, line 216:

<comment>`handleResume` doesn't guard against `connectionState === "connecting"`, so it calls `reconnectNow` even when a pre-flight connection attempt is already in progress (socket is still `null` during async relay affinity). This creates a race where both the watchdog-initiated and the online-event-initiated `connect()` calls proceed to `openSocket()`, producing two simultaneous WebSocket connections with the first being overwritten without close — leaving an orphaned socket on client and server.

Add a guard before calling `reconnectNow`:
```ts
if (transport.connectionState === "connecting") return;
```</comment>

<file context>
@@ -153,9 +160,103 @@ export function createTransport(): TerminalTransport {
+	) {
+		return;
+	}
+	reconnectNow(transport);
+}
+
</file context>
Suggested change
reconnectNow(transport);
if (transport.connectionState === "connecting") return;
reconnectNow(transport);

@saddlepaddle
saddlepaddle merged commit 38e6a2c into main Jun 5, 2026
9 of 10 checks passed
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ⚠️ Neon database branch

Thank you for your contribution! 🎉

saddlepaddle added a commit that referenced this pull request Jun 5, 2026
handleResume only skipped when transport.socket was OPEN/CONNECTING, but
for /hosts/ URLs connect() runs primeRelayAffinity async, leaving
transport.socket null while connectionState is "connecting". A resume
event (online/focus/visibilitychange) firing in that window saw the null
socket, fell through, and called reconnectNow a second time — both
pre-flights resolved into openSocket() and the first WebSocket was
overwritten without being closed, orphaning it on client and server.

Skip when connectionState is "connecting" too. reconnectNow stays
unguarded so the watchdog can still force-recycle a half-open "open"
socket (or a hung connect).

Follow-up to #5135 (flagged in review).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Remote terminal freezes after laptop sleep/wake (half-open WebSocket is never recovered)

1 participant