Skip to content

fix(desktop): guard terminal resume against in-flight connect - #5137

Merged
saddlepaddle merged 1 commit into
mainfrom
fix/desktop-terminal-resume-connecting-race
Jun 5, 2026
Merged

saddlepaddle merged 1 commit into
mainfrom
fix/desktop-terminal-resume-connecting-race

Conversation

@saddlepaddle

@saddlepaddle saddlepaddle commented Jun 5, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #5135, addressing the race flagged by Greptile (P1) and cubic (P2) in review.

Problem

handleResume only bailed when transport.socket was OPEN/CONNECTING. But for remote /hosts/ URLs, connect() runs primeRelayAffinity asynchronously before opening the socket, so there's a window where transport.socket is null while connectionState is "connecting".

Wake-time sequence that orphans a socket:

  1. watchdog fires → reconnectNow → connect() starts the async pre-flight → transport.socket is null, state "connecting"
  2. an online/focus/visibilitychange event fires → handleResume sees the null socket, skips the early-return, calls reconnectNow again
  3. both pre-flights resolve and each calls openSocket() (both pass the currentUrl/connectionState guards), opening two WebSockets — the first is overwritten in transport.socket without being closed, leaving it orphaned on client and server.

Fix

Bail out of handleResume when connectionState === "connecting" too, covering the pre-flight window. reconnectNow is deliberately left unguarded so the watchdog can still force-recycle a genuinely half-open socket (which reports "open") or a hung connect.

Testing

bun test (3 pass), bun run lint (clean), bun run typecheck (pass).


Open in Stage

Summary by cubic

Prevent double WebSocket openings during terminal resume by bailing out when a connect is already in flight. This avoids orphaned sockets on remote /hosts/ connections.

  • Bug Fixes
    • In handleResume, return early when connectionState === "connecting" or the socket is OPEN, covering the pre-flight window before transport.socket is set.

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

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved terminal connection handling to prevent duplicate reconnections by better checking the connection state before attempting to reconnect.

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).
@ghost

ghost commented Jun 5, 2026

Copy link
Copy Markdown

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

Title
1 Guard terminal resume against in-flight connections
Open in Stage

Chapters generated by Stage for commit e2d2cb7 on Jun 5, 2026 3:27am 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: 4dca96a8-1a2c-4cfa-8101-eb131b5b75d4

📥 Commits

Reviewing files that changed from the base of the PR and between 38e6a2c and e2d2cb7.

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

📝 Walkthrough

Walkthrough

The handleResume() method in the terminal WebSocket transport now checks two conditions before attempting a reconnect: it early-returns if a connection attempt is already in progress via connectionState === "connecting" or if the socket is already open. This replaces the prior logic that relied solely on socket ready state.

Changes

Resume Reconnect Suppression

Layer / File(s) Summary
Resume reconnect suppression refinement
apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.ts
handleResume() now suppresses reconnect attempts by checking both transport state (connectionState === "connecting") and socket state (WebSocket.OPEN), replacing the previous socket-only ready-state check.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related issues

  • superset-sh/superset#5130: Addresses resume and reconnect behavior for half-open WebSockets in the same terminal transport implementation.

Possibly related PRs

  • superset-sh/superset#5135: Also refines terminal-ws-transport.ts resume/reconnect handling by adding sleep/wake liveness checks; this PR further refines the suppression logic with transport state validation.

Poem

🐰 A hop and a reconnect, no double attempt,
When the line's still connecting, we prudently skip it—
Keep your sockets wide open and state machines clean,
For the smoothest resume that you've ever seen! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding a guard to handle terminal resume when a connection is already in-flight (connecting state).
Description check ✅ Passed The description includes a detailed Problem section, Fix explanation, Testing results, and follows the template structure with clear context about the race condition and solution.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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-resume-connecting-race

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.

@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.

No issues found across 1 file

Re-trigger cubic

@greptile-apps

greptile-apps Bot commented Jun 5, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR tightens the guard in handleResume so that a DOM resume event (online, focus, visibilitychange) no longer triggers reconnectNow while a connection is already in flight. The fix replaces a socket.readyState-based check with a connectionState === "connecting" check, which also covers the async pre-flight window for /hosts/ URLs where transport.socket is still null.

  • Race closed: handleResume now bails immediately if connectionState === "connecting", preventing a second call to reconnectNow that would have kicked off a parallel pre-flight and ultimately opened two WebSockets — with the first one orphaned (never closed).
  • Watchdog unchanged: reconnectNow itself remains unguarded, so the wall-clock liveness watchdog can still force-recycle a genuinely half-open socket even when connectionState reports "open" or a hung "connecting".
  • Dropped check is safe: The removed socket.readyState === WebSocket.CONNECTING branch is fully covered by connectionState === "connecting", since the state is set before openSocket() is ever called and only transitions away when the "attached" message arrives or the socket closes.

Confidence Score: 4/5

Safe to merge; the one-line guard change is mechanically correct and the rest of the transport logic is untouched.

The fix correctly closes the pre-flight race window by checking connectionState instead of socket.readyState, and the dropped WebSocket.CONNECTING branch is fully covered. The only gap is that no regression test pins the specific invariant being fixed — a future refactor that reorders setConnectionState and openSocket could silently re-open the race without a failing test to catch it.

apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.ts — verify the pre-flight race scenario is captured by a new test

Important Files Changed

Filename Overview
apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.ts Adds connectionState === 'connecting' guard to handleResume, closing the race window where the /hosts/ async pre-flight leaves transport.socket null while a wake/resume event could call reconnectNow a second time and open a duplicate WebSocket. The logic is correct and the removed WebSocket.CONNECTING readyState check is safely subsumed by the connectionState guard.

Sequence Diagram

sequenceDiagram
    participant WD as Watchdog / reconnectNow
    participant C as connect()
    participant PF as primeRelayAffinity
    participant OS as openSocket()
    participant HR as handleResume

    WD->>C: reconnectNow → connect()
    C->>C: setConnectionState("connecting")
    C->>PF: await primeRelayAffinity(url) [async]
    Note over C,PF: transport.socket = null, connectionState = "connecting"

    Note over HR: online/focus event fires
    HR->>HR: "check connectionState === "connecting" → bail ✓"
    Note over HR: old code saw socket=null, fell through, called reconnectNow again ✗

    PF-->>OS: resolves → openSocket()
    OS->>OS: new WebSocket(url)
    OS->>OS: "transport.socket = socket"
    Note over OS: Only one socket created
Loading

Comments Outside Diff (1)

  1. apps/desktop/src/renderer/lib/terminal/terminal-ws-transport.ts, line 204-219 (link)

    P2 Missing regression test for the guarded race

    The existing test suite doesn't cover the specific scenario this PR fixes: a resume event firing while connectionState === "connecting" and transport.socket === null (i.e., during the /hosts/ pre-flight window). Without a test, a future refactor that moves setConnectionState(transport, "connecting") after openSocket() would silently re-introduce the orphaned-socket race. A test that mocks primeRelayAffinity to return a deferred promise, fires a window online event mid-flight, then asserts MockWebSocket.instances.length === 1 after both promises resolve would pin this invariant.

    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: 204-219
    
    Comment:
    **Missing regression test for the guarded race**
    
    The existing test suite doesn't cover the specific scenario this PR fixes: a resume event firing while `connectionState === "connecting"` and `transport.socket === null` (i.e., during the `/hosts/` pre-flight window). Without a test, a future refactor that moves `setConnectionState(transport, "connecting")` after `openSocket()` would silently re-introduce the orphaned-socket race. A test that mocks `primeRelayAffinity` to return a deferred promise, fires a window `online` event mid-flight, then asserts `MockWebSocket.instances.length === 1` after both promises resolve would pin this invariant.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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:204-219
**Missing regression test for the guarded race**

The existing test suite doesn't cover the specific scenario this PR fixes: a resume event firing while `connectionState === "connecting"` and `transport.socket === null` (i.e., during the `/hosts/` pre-flight window). Without a test, a future refactor that moves `setConnectionState(transport, "connecting")` after `openSocket()` would silently re-introduce the orphaned-socket race. A test that mocks `primeRelayAffinity` to return a deferred promise, fires a window `online` event mid-flight, then asserts `MockWebSocket.instances.length === 1` after both promises resolve would pin this invariant.

Reviews (1): Last reviewed commit: "fix(desktop): guard terminal resume agai..." | Re-trigger Greptile

@saddlepaddle
saddlepaddle merged commit 9f2b28f 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! 🎉

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.

1 participant