Skip to content

[FTF-198] Send close code on react-web-cli panel unmount#128

Open
denissellu wants to merge 1 commit intomainfrom
FTF-198-send-close-code-on-unmount
Open

[FTF-198] Send close code on react-web-cli panel unmount#128
denissellu wants to merge 1 commit intomainfrom
FTF-198-send-close-code-on-unmount

Conversation

@denissellu
Copy link
Member

@denissellu denissellu commented Jan 9, 2026

See FTF-198 This PR ensures the terminal component signals session closure correctly over WebSocket, while preserving session resume on navigation/reload.

Companion server PR: https://github.com/ably/cli-terminal-server/pull/105


Problem

The original implementation sent close code 4001 on every component unmount. This told the server to immediately destroy the session — but unmount also fires on page navigation and reloads, which broke resumeOnReload behavior. Additionally, the close handler didn't recognize the custom close reason, causing error UI to flash before unmount.

Solution

Unmount calls grCancelReconnect() then close() with no special code. This prevents post-unmount reconnect attempts while preserving the server grace period for session resume.

terminateSession() — a new method on the AblyCliTerminalHandle ref — sends close(4001, "user-closed-primary") / close(4001, "user-closed-secondary"). These use existing recognized close reasons so handleWebSocketClose returns early (no error UI, no sessionId clearing), while the server still receives the 4001 code for immediate cleanup.

Changes

File What changed
AblyCliTerminal.tsx Unmount calls grCancelReconnect() before close(); terminateSession() uses recognized close reasons
AblyCliTerminal.test.tsx Tests use correct signedConfig/signature props and waitFor instead of setTimeout

Usage

const terminalRef = useRef<AblyCliTerminalHandle>(null);

// When user clicks "close panel":
terminalRef.current?.terminateSession();
// Then unmount the component

Note

Medium Risk
Changes WebSocket lifecycle/close semantics and introduces a new imperative API; mistakes could cause session leaks or break resume/reconnect behavior, but scope is limited to the terminal component and is covered by new unit tests.

Overview
Adjusts AblyCliTerminal unmount behavior to cancel pending reconnects and then close() the WebSocket without the special 4001 code, preventing unmount/navigation from forcing server-side session destruction and improving resumeOnReload support.

Adds a new imperative handle method terminateSession() that explicitly closes the primary/secondary sockets with close(4001, "user-closed-*") for immediate server cleanup when the user intentionally closes the panel.

Updates tests to cover unmount cleanup semantics and validate the new terminateSession() close code/reason behavior.

Written by Cursor Bugbot for commit 22e6b65. This will update automatically on new commits. Configure here.

@vercel
Copy link

vercel bot commented Jan 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cli-web-cli Ready Ready Preview, Comment Mar 9, 2026 10:10am

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Jan 9, 2026

Walkthrough

Modifies AblyCliTerminal component to close WebSocket connections with code 4001 and reason "user-closed-panel" when unmounting, signaling intentional shutdown to enable server session cleanup. Adds test coverage for unmount cleanup behavior.

Changes

Cohort / File(s) Change Summary
WebSocket close signal on unmount
packages/react-web-cli/src/AblyCliTerminal.tsx
Updated WebSocket close call on component unmount to pass close code 4001 with reason "user-closed-panel" instead of plain close, with explanatory comment for server cleanup intent
Unmount cleanup test suite
packages/react-web-cli/src/AblyCliTerminal.test.tsx
Added new "Unmount cleanup" test suite with two test cases: one verifying WebSocket closes with code 4001 and reason when unmounted, another verifying no close call when socket already in CLOSING state

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 When panels close and time to part,
A code four-zero-zero-one at heart,
"User-closed-panel" rings so clear,
Sessions end without a tear! 🌙✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: sending a close code when the react-web-cli panel unmounts.
Linked Issues check ✅ Passed Changes implement the core requirement to terminate terminal sessions on panel unmount by closing WebSocket with code 4001 and reason 'user-closed-panel'.
Out of Scope Changes check ✅ Passed All changes are directly related to the issue objective of terminating sessions on panel close; no out-of-scope modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch FTF-198-send-close-code-on-unmount

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/react-web-cli/src/AblyCliTerminal.tsx (1)

3556-3564: Add close code and reason to secondary WebSocket cleanup for consistency.

The secondary WebSocket at line 3563 closes without an explicit close code, defaulting to 1000, while the primary socket uses close code 4001 with reason "user-closed-panel" at line 2425. This inconsistency should be addressed.

Consider updating line 3563 to use an explicit close code and reason string (e.g., close(1000, "user-closed-panel") or close(4001, "user-closed-panel")) to match the pattern established for primary socket cleanup. This ensures both sockets signal their closure to the server in the same manner.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9b37916 and f7a6f37.

📒 Files selected for processing (2)
  • packages/react-web-cli/src/AblyCliTerminal.test.tsx
  • packages/react-web-cli/src/AblyCliTerminal.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/Development.mdc)

**/*.{ts,tsx}: Use TypeScript and follow best practice naming conventions
ESLint is used to ensure all code adheres best practices; ensure you check that all code passes the linter before concluding your work.

Files:

  • packages/react-web-cli/src/AblyCliTerminal.tsx
  • packages/react-web-cli/src/AblyCliTerminal.test.tsx
🧬 Code graph analysis (2)
packages/react-web-cli/src/AblyCliTerminal.tsx (1)
packages/react-web-cli/src/terminal-shared.ts (1)
  • debugLog (311-315)
packages/react-web-cli/src/AblyCliTerminal.test.tsx (1)
packages/react-web-cli/src/AblyCliTerminal.tsx (1)
  • AblyCliTerminal (3838-3841)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: auth-tests
  • GitHub Check: session-tests
  • GitHub Check: ui-tests
  • GitHub Check: Cursor Bugbot
  • GitHub Check: e2e-cli
🔇 Additional comments (2)
packages/react-web-cli/src/AblyCliTerminal.tsx (1)

2418-2426: LGTM - Clear intent and proper cleanup.

The unmount logic correctly closes the WebSocket with code 4001 to signal intentional closure, and the guard condition properly checks for sockets that are still active (CONNECTING or OPEN states). The inline comment clearly documents the server-side cleanup behavior.

packages/react-web-cli/src/AblyCliTerminal.test.tsx (1)

2471-2514: LGTM - Comprehensive test coverage for unmount cleanup.

The test suite appropriately covers both the standard unmount case (socket open → close with 4001) and the edge case where the socket is already closing. The test structure is clear and the assertions directly verify the implementation's behavior.

// This tells the server to cleanup immediately (no grace period)
debugLog("[AblyCLITerminal] Closing WebSocket on unmount.");
socketReference.current.close();
socketReference.current.close(4001, "user-closed-panel");
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe the intention here is that if we send 4001 to the terminal server, it just should just remove the session immediately, rather than wait for natural timeout. This is fine in principle for when a user explicitly clicks something, but doing this here would force the close on any unmount, which could happen between pages? We certainly don't want to limit navigation options to just non-reloading.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adjusts AblyCliTerminal WebSocket close behavior to preserve resumable sessions on navigation/reload, while still allowing explicit “destroy session now” behavior when the user closes the panel.

Changes:

  • Change unmount cleanup to perform a normal WebSocket close (no 4001) to preserve server-side resume grace period.
  • Add terminateSession() to the AblyCliTerminalHandle imperative API to explicitly close with 4001 and a reason.
  • Add unit tests for unmount close behavior and terminateSession() close semantics.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
packages/react-web-cli/src/AblyCliTerminal.tsx Adds terminateSession() and changes unmount close from a special code to a normal close for resume support.
packages/react-web-cli/src/AblyCliTerminal.test.tsx Adds tests validating unmount cleanup and explicit session termination behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

) {
// close websocket
// Normal close (no 4001) so server grace period allows resume
debugLog("[AblyCLITerminal] Closing WebSocket on unmount.");
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

Calling socketReference.current.close() on unmount will produce a normal CloseEvent (likely code 1000, empty reason). In handleWebSocketClose, that path is treated as recoverable and schedules auto-reconnect, which can continue after the component has unmounted (since grResetState() does not cancel timers unless already cancelled). Consider ensuring unmount close is treated as a user-initiated/no-reconnect close (e.g., close with an explicit reason that handleWebSocketClose short-circuits on, or set an isUnmounting flag checked by the close handler / cancel reconnect timers before closing).

Suggested change
debugLog("[AblyCLITerminal] Closing WebSocket on unmount.");
debugLog("[AblyCLITerminal] Closing WebSocket on unmount.");
// Prevent auto-reconnect and handler side effects after unmount
grCancelReconnect();
socketReference.current.onclose = null;
socketReference.current.onerror = null;

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

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

@coderabbitai what do you think about above review comment, can you explain a bit more

Copy link

Choose a reason for hiding this comment

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

@sacOO7 Great question! Let me break down Copilot's concern and the broader context here.

Two Different Scenarios

Based on the code, there are two distinct ways the WebSocket gets closed:

  1. Explicit termination (via terminateSession() method):

    • Uses code 4001 and reason "user-closed-panel"
    • Signals to the server that the session should be terminated immediately
    • Intended for when the user actively closes the panel
  2. Component unmount (cleanup in useEffect):

    • Currently uses normal close (code 1000, no reason)
    • Allows the server to keep the session alive with a grace period for potential resumption
    • Happens when the component is unmounted (e.g., page navigation, React re-render)

The Problem Copilot Identified

When the component unmounts and calls socketReference.current.close() without arguments, it produces a CloseEvent with code 1000 (normal closure) and an empty reason.

Looking at your handleWebSocketClose function (around line 1645), it checks for specific reasons to avoid reconnecting:

const userClosedTerminal =
  event.reason === "user-closed-primary" ||
  event.reason === "user-closed-secondary" ||
  event.reason === "manual-reconnect";

Since the unmount close has no reason and code 1000 is not in your NON_RECOVERABLE_CLOSE_CODES set, the close event could potentially fall through to the reconnection logic. This means:

  • Reconnection timers might be scheduled
  • The component is already unmounted, so any callbacks would reference stale DOM/refs
  • Resource leak and potential errors

Current Mitigation

I notice your unmount cleanup does call grResetState() at the end:

grResetState(); // Ensure global state is clean
clearConnectionTimeout();

Key question: Does grResetState() properly cancel any pending reconnection timers? If yes, this concern is mitigated. If not, there's a potential issue.

Recommended Solutions

Option 1: Add a specific close reason for unmount to prevent reconnection logic from firing:

// Close with a reason that handleWebSocketClose recognizes as non-reconnectable
socketReference.current.close(1000, "component-unmount");

Then update handleWebSocketClose to recognize it:

const userClosedTerminal =
  event.reason === "user-closed-primary" ||
  event.reason === "user-closed-secondary" ||
  event.reason === "manual-reconnect" ||
  event.reason === "component-unmount";

Option 2: Explicitly cancel reconnection and remove event listeners before closing:

grCancelReconnect(); // Cancel any pending reconnect timers
if (socketReference.current) {
  // Note: Since you use addEventListener, you'd need to remove listeners
  // rather than set onclose = null
  socketReference.current.close();
}
grResetState();

The design intent (normal close to allow resume) is good, but you need to ensure the close handler doesn't trigger unwanted reconnection attempts after unmount. Let me know if you'd like me to search for the implementation of grResetState to verify it cancels timers properly!

Copy link
Contributor

Choose a reason for hiding this comment

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

@denissellu can you take a look

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +353 to +359
socketReference.current.close(4001, "user-closed-panel");
}
if (
secondarySocketReference.current &&
secondarySocketReference.current.readyState < WebSocket.CLOSING
) {
secondarySocketReference.current.close(4001, "user-closed-panel");
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

terminateSession() closes with reason "user-closed-panel", but handleWebSocketClose only treats "user-closed-primary"/"user-closed-secondary"/"manual-reconnect" as user-initiated. As a result, a 4001 close here will run the non-recoverable path and may briefly render a disconnect/error UI and trigger state updates just before unmount. Either include this reason in the user-initiated branch (and explicitly clear stored session IDs there) or adjust the close handling so explicit panel termination doesn’t surface as an error.

Suggested change
socketReference.current.close(4001, "user-closed-panel");
}
if (
secondarySocketReference.current &&
secondarySocketReference.current.readyState < WebSocket.CLOSING
) {
secondarySocketReference.current.close(4001, "user-closed-panel");
socketReference.current.close(4001, "user-closed-primary");
}
if (
secondarySocketReference.current &&
secondarySocketReference.current.readyState < WebSocket.CLOSING
) {
secondarySocketReference.current.close(4001, "user-closed-secondary");

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

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

@coderabbitai what do you think about above review comment, can you explain a bit more

Copy link

Choose a reason for hiding this comment

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

@sacOO7, let me explain the issue Copilot identified:

The Problem

When terminateSession() is called (typically during unmount), it closes the WebSocket with:

  • Code: 4001
  • Reason: "user-closed-panel"

However, in the handleWebSocketClose function, there's a check for user-initiated closes:

const userClosedTerminal =
  event.reason === "user-closed-primary" ||
  event.reason === "user-closed-secondary" ||
  event.reason === "manual-reconnect";

if (userClosedTerminal) {
  // Exit early - no reconnection, no error UI
  return;
}

Since "user-closed-panel" is not in this list, the close event continues through the handler. Then it hits this check:

const NON_RECOVERABLE_CLOSE_CODES = new Set<number>([
  4001, // Policy violation
  // ...
]);

if (NON_RECOVERABLE_CLOSE_CODES.has(event.code)) {
  // Shows error UI, sets manual reconnect prompt, etc.
}

The Consequence

When the component unmounts and terminateSession() is called:

  1. WebSocket closes with code 4001 and reason "user-closed-panel"
  2. handleWebSocketClose doesn't recognize this as user-initiated
  3. It enters the non-recoverable error path
  4. Shows error overlay/UI and triggers state updates
  5. All this happens right before the component unmounts (unnecessary work)

The Solution

Instead of changing the close reason (as Copilot suggested), I'd recommend adding "user-closed-panel" to the user-initiated check. This preserves the semantic clarity of the close reason while fixing the handling:

const userClosedTerminal =
  event.reason === "user-closed-primary" ||
  event.reason === "user-closed-secondary" ||
  event.reason === "manual-reconnect" ||
  event.reason === "user-closed-panel";  // Add this line

This way:

  • The server still receives code 4001 with reason "user-closed-panel" for immediate cleanup (as per your PR objectives)
  • The client recognizes it as an intentional user action
  • No error UI or unnecessary state updates during unmount
  • The reason remains semantically meaningful ("user-closed-panel" is clearer than "user-closed-primary")

Would you like me to search for the exact location in the file where this change should be made?

Copy link
Contributor

Choose a reason for hiding this comment

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

@denissellu can you take a look

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +2565 to +2569
const { unmount } = render(
<AblyCliTerminal
websocketUrl="wss://test.ably.com"
ablyApiKey="test-key"
/>,
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

These new tests render AblyCliTerminal with ablyApiKey, but the component props interface in this package expects signedConfig + signature (and there is no ablyApiKey prop). This will fail type-checking and likely won’t exercise the real auth/connection path. Update the tests to use the existing renderTerminal helper/fixtures (DEFAULT_SIGNED_CONFIG / DEFAULT_SIGNATURE / TEST_WEBSOCKET_URL) so they compile and match the component’s supported API.

Copilot uses AI. Check for mistakes.
Comment on lines +2572 to +2574
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 50));
});
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The tests rely on setTimeout(..., 50) to wait for the socket to open. Elsewhere in this file you use waitFor assertions on mockSocketInstance.readyState === WebSocket.OPEN, which is less timing/flakiness-prone. Consider switching these waits to waitFor to reduce intermittent failures in CI.

Copilot uses AI. Check for mistakes.
…t graceful

Unmount calls grCancelReconnect() then close() with no special code,
preventing post-unmount reconnect attempts while preserving the server
grace period for session resume.

terminateSession() on the ref handle sends close(4001) with recognized
user-closed reasons so the close handler exits early without showing
error UI or clearing session state.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Free Tier Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Unmount sets stale isCancelled breaking retry on remount
    • I added an explicit cancellation reset path to grResetState and used it in the initial connect effect so remounts start with isCancelled=false and retries proceed normally after a failure.
  • ✅ Fixed: terminateSession leaves stale sessionId in session storage
    • terminateSession() now clears the domain-scoped persisted session/credential entries and local sessionId state before closing with code 4001, preventing stale resume attempts.

Create PR

Or push these changes by commenting:

@cursor push 7bd1a97fda
Preview (7bd1a97fda)
diff --git a/packages/react-web-cli/src/AblyCliTerminal.tsx b/packages/react-web-cli/src/AblyCliTerminal.tsx
--- a/packages/react-web-cli/src/AblyCliTerminal.tsx
+++ b/packages/react-web-cli/src/AblyCliTerminal.tsx
@@ -346,6 +346,16 @@
         debugLog(
           "[AblyCLITerminal] terminateSession called - closing with code 4001",
         );
+        if (resumeOnReload && globalThis.window !== undefined) {
+          const urlDomain = new URL(websocketUrl).host;
+          globalThis.sessionStorage.removeItem(
+            `ably.cli.sessionId.${urlDomain}`,
+          );
+          globalThis.sessionStorage.removeItem(
+            `ably.cli.credentialHash.${urlDomain}`,
+          );
+          setSessionId(null);
+        }
         if (
           socketReference.current &&
           socketReference.current.readyState < WebSocket.CLOSING
@@ -366,6 +376,8 @@
       handleCloseSplit,
       isSplit,
       splitPosition,
+      resumeOnReload,
+      websocketUrl,
     ],
   );
 
@@ -2422,7 +2434,7 @@
       grSetMaxAttempts(maxReconnectAttempts);
     }
 
-    grResetState();
+    grResetState({ resetCancellation: true });
     clearPtyBuffer();
     connectWebSocket();
   }, [

diff --git a/packages/react-web-cli/src/global-reconnect.ts b/packages/react-web-cli/src/global-reconnect.ts
--- a/packages/react-web-cli/src/global-reconnect.ts
+++ b/packages/react-web-cli/src/global-reconnect.ts
@@ -30,17 +30,16 @@
 /**
  * Reset the reconnection state
  */
-export function resetState(): void {
+export function resetState(options?: { resetCancellation?: boolean }): void {
   console.log(
-    `[GlobalReconnect] resetState called. Current attempts (before potential reset): ${attempts}, isCancelled: ${isCancelled}`,
+    `[GlobalReconnect] resetState called. Current attempts (before potential reset): ${attempts}, isCancelled: ${isCancelled}, resetCancellation: ${Boolean(options?.resetCancellation)}`,
   );
   // ATTEMPTS ARE NO LONGER RESET HERE. They should be reset by a successful connection
   // or an explicit action to start a new connection sequence like cancelReconnect.
   // attempts = 0;
 
-  // Only clear timers if fully cancelling. If just a transient reset before a new schedule,
-  // scheduleReconnect will handle its own timer. isCancelled drives this.
-  if (isCancelled) {
+  // Clear timers when cancelled, or when explicitly resetting cancellation for a fresh connection sequence.
+  if (isCancelled || options?.resetCancellation) {
     if (countdownTimer) {
       console.log(
         "[GlobalReconnect] resetState: Clearing countdownTimer because isCancelled=true",
@@ -57,7 +56,11 @@
     }
   }
   remainingTimeMs = 0;
-  // isCancelled is NOT reset here. It's reset by successfulConnectionReset or explicitly by scheduleReconnect if not at max attempts.
+  // isCancelled is NOT reset by default. It's reset by successfulConnectionReset,
+  // explicitly by scheduleReconnect if not at max attempts, or via resetCancellation.
+  if (options?.resetCancellation) {
+    isCancelled = false;
+  }
 }
 
 /**
This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

term.current.dispose();
term.current = null;
}
grCancelReconnect();
Copy link

Choose a reason for hiding this comment

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

Unmount sets stale isCancelled breaking retry on remount

High Severity

Calling grCancelReconnect() on unmount sets global isCancelled = true. On remount, the connection effect calls grResetState() which never resets isCancelled back to false. If the first connection attempt after remount fails, both handleWebSocketError and handleWebSocketClose check grIsCancelledState(), see it's true, and skip automatic retry — showing a "reconnect cancelled" error instead. The old code used grResetState() on unmount, which didn't set isCancelled, avoiding this problem.

Fix in Cursor Fix in Web

) {
secondarySocketReference.current.close(4001, "user-closed-secondary");
}
},
Copy link

Choose a reason for hiding this comment

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

terminateSession leaves stale sessionId in session storage

Medium Severity

terminateSession() sends close code 4001, which tells the server to destroy the session immediately. However, handleWebSocketClose returns early for the "user-closed-primary" reason without clearing the sessionId from sessionStorage. When resumeOnReload is enabled, a subsequent remount or page reload will attempt to resume the destroyed session, triggering a 4002 (session resume rejected) error shown to the user.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

5 participants