Skip to content

fix(desktop): classify quit-time terminal-host disposal as typed non-500 - #6197

Merged
saddlepaddle merged 2 commits into
mainfrom
sentry/quit-connection-lost
Aug 5, 2026
Merged

saddlepaddle merged 2 commits into
mainfrom
sentry/quit-connection-lost

Conversation

@saddlepaddle

@saddlepaddle saddlepaddle commented Aug 5, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Refs DESKTOP-HD: a normal app quit racing an in-flight terminal request was being reported as a 500.

During quit, runQuitCleanup → disposeTerminalHostClient → dispose → disconnect → resetConnectionState rejected every pending terminal-host request with a generic Error("Connection lost"). An in-flight terminal.createOrAttach mutation then surfaced that as INTERNAL_SERVER_ERROR, which the Sentry tRPC middleware captures.

Changes

Per the error-handling contract (#6164 — typed causes at throw sites, never a Sentry filter):

  • TerminalHostClient.disconnect() (the intentional teardown path: dispose/shutdown) now rejects pending requests with a typed TerminalHostClientDisposedError instead of the generic error.
  • The terminal.createOrAttach catch translates that error to PRECONDITION_FAILED with cause: { kind: "TERMINAL_HOST_CLIENT_DISPOSED" }, so it never reaches the Sentry middleware as a 500.
  • Genuine unexpected connection loss (handleDisconnect on socket drop, connect-failure resets) keeps the generic Error("Connection lost") rejection and still reports.

Verification

  • bunx biome check clean on both changed files
  • cd apps/desktop && bun run typecheck passes

https://claude.ai/code/session_017sk3KSZ5NLrpTedKWRX2k8


Summary by cubic

Prevent normal app quit from being reported as a 500 when a terminal request is in flight. Quit-time terminal-host teardown now uses a typed error and maps to a non-500 tRPC code.

  • Bug Fixes
    • Added TerminalHostClientDisposedError; used on explicit disconnect/dispose to reject pending requests.
    • In terminal.createOrAttach, translate this to PRECONDITION_FAILED with cause: { kind: "TERMINAL_HOST_CLIENT_DISPOSED" }; unexpected socket drops keep the generic "Connection lost" and still report.

Written for commit 8ed37ff. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved terminal connection error handling during intentional shutdowns.
    • Standardized error responses when terminal sessions are disposed.
    • Preserved clear “connection lost” messaging for unexpected disconnects.
    • Improved handling of pending terminal requests during connection resets.

During app quit, runQuitCleanup disposes the terminal-host client while a
terminal.createOrAttach mutation can still be in flight. resetConnectionState
rejected every pending request with a generic Error("Connection lost"), which
surfaced as INTERNAL_SERVER_ERROR and got captured by the Sentry tRPC
middleware (DESKTOP-HD) even though the teardown is intentional.

Reject pending requests with TerminalHostClientDisposedError on the explicit
disconnect/dispose path only, and translate it at the tRPC boundary to
PRECONDITION_FAILED with a typed cause. Unexpected socket drops
(handleDisconnect) keep the generic rejection and still report.

Claude-Session: https://claude.ai/code/session_017sk3KSZ5NLrpTedKWRX2k8
@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The terminal host client now reports intentional disposal separately from unexpected connection loss. The terminal router applies shared middleware to terminal procedures and maps disposal failures to a PRECONDITION_FAILED tRPC error.

Changes

Terminal disposal error handling

Layer / File(s) Summary
Client disposal error propagation
apps/desktop/src/main/lib/terminal-host/client.ts
The client defines TerminalHostClientDisposedError. Connection reset can reject pending requests with this error, while unexpected disconnects retain "Connection lost".
Router disposal error mapping
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
Terminal procedures use shared middleware. createOrAttach rethrows disposal errors so the middleware returns PRECONDITION_FAILED with standardized metadata.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title uses conventional commit format and clearly describes the typed non-500 handling for terminal-host disposal during app quit.
Description check ✅ Passed The description explains the problem, changes, error-handling behavior, and verification steps, but it omits the template checklist.
✨ 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 sentry/quit-connection-lost

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.

@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR distinguishes intentional terminal-host teardown from unexpected connection loss and maps quit-time createOrAttach disposal to a typed non-500 response.

  • Adds TerminalHostClientDisposedError and uses it when explicit disconnect rejects pending requests.
  • Translates that error to PRECONDITION_FAILED in the createOrAttach router path.
  • The translation does not yet cover other terminal-host requests rejected by the same disconnect.

Confidence Score: 4/5

The PR should not merge until intentional disposal is translated consistently for every terminal-host request that can be pending during quit.

Explicit disconnect now rejects all pending request methods with the new typed error, while only createOrAttach handles it; other reachable terminal procedures can therefore continue producing the quit-time internal errors this change is intended to eliminate.

Files Needing Attention: apps/desktop/src/main/lib/terminal-host/client.ts and apps/desktop/src/lib/trpc/routers/terminal/terminal.ts

Important Files Changed

Filename Overview
apps/desktop/src/main/lib/terminal-host/client.ts Adds a typed intentional-disposal rejection, but applies it to every pending request even though router translation currently covers only one request path.
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts Correctly maps disposal during createOrAttach to a non-500 response, while sibling terminal procedures remain able to leak the same typed error as a 500.

Sequence Diagram

sequenceDiagram
  participant UI as Renderer
  participant RPC as Terminal tRPC router
  participant Client as TerminalHostClient
  participant Quit as Quit cleanup
  participant Sentry

  UI->>RPC: clearScrollback/listSessions request
  RPC->>Client: sendRequest()
  Note over Client: Request stored in pendingRequests
  Quit->>Client: dispose() / disconnect()
  Client-->>RPC: TerminalHostClientDisposedError
  Note over RPC: Only createOrAttach translates this error
  RPC-->>UI: INTERNAL_SERVER_ERROR
  RPC->>Sentry: captureException()
Loading
Prompt To Fix All With AI
### Issue 1
apps/desktop/src/main/lib/terminal-host/client.ts:819
**Disposal errors still escape as 500s**

If another terminal-host request such as `clearScrollback` or `listDaemonSessions` is awaiting a daemon response during quit, `disconnect()` rejects it with `TerminalHostClientDisposedError`, but only `createOrAttach` translates that error. The unhandled rejection therefore becomes `INTERNAL_SERVER_ERROR` and is still captured by Sentry.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(desktop): classify quit-time termina..." | Re-trigger Greptile

for (const [id, pending] of this.pendingRequests.entries()) {
clearTimeout(pending.timeoutId);
pending.reject(new Error("Connection lost"));
pending.reject(pendingRequestError ?? new Error("Connection lost"));

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 Disposal errors still escape as 500s

If another terminal-host request such as clearScrollback or listDaemonSessions is awaiting a daemon response during quit, disconnect() rejects it with TerminalHostClientDisposedError, but only createOrAttach translates that error. The unhandled rejection therefore becomes INTERNAL_SERVER_ERROR and is still captured by Sentry.

Knowledge Base Used: Desktop App (Electron main + renderer)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/lib/terminal-host/client.ts
Line: 819

Comment:
**Disposal errors still escape as 500s**

If another terminal-host request such as `clearScrollback` or `listDaemonSessions` is awaiting a daemon response during quit, `disconnect()` rejects it with `TerminalHostClientDisposedError`, but only `createOrAttach` translates that error. The unhandled rejection therefore becomes `INTERNAL_SERVER_ERROR` and is still captured by Sentry.

**Knowledge Base Used:** [Desktop App (Electron main + renderer)](https://app.greptile.com/personal-org-6810/-/custom-context/knowledge-base/superset-sh/superset/-/docs/desktop-app.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/main/lib/terminal-host/client.ts`:
- Around line 784-787: Extend the shared reset/disposal flow around
resetConnectionState, sendRequestOnStream, and ensureConnected to reject every
in-flight connection waiter and stream request with
TerminalHostClientDisposedError when disconnect() disposes the client. Ensure
ensureConnected checks disposal immediately before publishing CONNECTED so
pre-disconnect attempts cannot restore the client, while preserving generic
errors for unexpected socket loss.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 734d1d1e-8ae9-4001-b73d-ef01fa3df07f

📥 Commits

Reviewing files that changed from the base of the PR and between 21db9f5 and 8ed37ff.

📒 Files selected for processing (2)
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
  • apps/desktop/src/main/lib/terminal-host/client.ts

Comment on lines +784 to +787
pendingRequestError,
}: {
emitDisconnected: boolean;
pendingRequestError?: Error;

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Propagate disposal to every in-flight connection operation.

resetConnectionState only rejects entries in pendingRequests. sendRequestOnStream and the connection waiters in ensureConnected use separate promises. If disconnect() runs during stream authentication or connection setup, those paths can return Request timeout: hello or Connection failed while waiting. createOrAttach then bypasses the TerminalHostClientDisposedError mapping in apps/desktop/src/lib/trpc/routers/terminal/terminal.ts Lines 189-195.

Add a shared cancellation path for connection waiters and stream requests. Also guard ensureConnected before it publishes CONNECTED, so an attempt started before disconnect() cannot restore a disposed client. Preserve generic errors for unexpected socket loss.

Also applies to: 819-819, 1663-1666

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/main/lib/terminal-host/client.ts` around lines 784 - 787,
Extend the shared reset/disposal flow around resetConnectionState,
sendRequestOnStream, and ensureConnected to reject every in-flight connection
waiter and stream request with TerminalHostClientDisposedError when disconnect()
disposes the client. Ensure ensureConnected checks disposal immediately before
publishing CONNECTED so pre-disconnect attempts cannot restore the client, while
preserving generic errors for unexpected socket loss.

@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/main/lib/terminal-host/client.ts">

<violation number="1" location="apps/desktop/src/main/lib/terminal-host/client.ts:819">
P2: resetConnectionState only rejects entries in pendingRequests with the new typed TerminalHostClientDisposedError. sendRequestOnStream and the connection waiters in ensureConnected use separate promises that aren't covered here, so if disconnect() fires during stream auth or connection setup, callers can still see generic timeout/connection-failure errors that bypass the TerminalHostClientDisposedError mapping in createOrAttach and continue to surface as 500s.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Main as Desktop App (Main)
    participant Router as Terminal TRPC Router
    participant THC as TerminalHostClient
    participant Sentry as Sentry Middleware

    Note over Router, THC: In-flight Request Scenario
    Router->>THC: terminal.createOrAttach()
    THC->>THC: Store in pendingRequests

    alt Intentional Shutdown (e.g. App Quit)
        Main->>THC: disconnect() / dispose()
        THC->>THC: CHANGED: resetConnectionState() with TerminalHostClientDisposedError
        THC-->>Router: Reject pending request (DisposedError)
        
        Router->>Router: NEW: Catch TerminalHostClientDisposedError
        Router-->>Sentry: NEW: Throw TRPCError (PRECONDITION_FAILED)
        Note over Sentry: Non-500 response: No Sentry alert triggered
        
    else Unexpected Connection Loss
        THC->>THC: Socket drops (handleDisconnect)
        THC->>THC: resetConnectionState() with default Error("Connection lost")
        THC-->>Router: Reject pending request (Generic Error)
        
        Router-->>Sentry: Throw TRPCError (INTERNAL_SERVER_ERROR)
        Note over Sentry: 500 response: Sentry captures & alerts
    end

    Sentry-->>Router: Response complete
    Router-->>Main: Return error to UI/Client
Loading

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

Re-trigger cubic

for (const [id, pending] of this.pendingRequests.entries()) {
clearTimeout(pending.timeoutId);
pending.reject(new Error("Connection lost"));
pending.reject(pendingRequestError ?? new Error("Connection lost"));

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: resetConnectionState only rejects entries in pendingRequests with the new typed TerminalHostClientDisposedError. sendRequestOnStream and the connection waiters in ensureConnected use separate promises that aren't covered here, so if disconnect() fires during stream auth or connection setup, callers can still see generic timeout/connection-failure errors that bypass the TerminalHostClientDisposedError mapping in createOrAttach and continue to surface as 500s.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/main/lib/terminal-host/client.ts, line 819:

<comment>resetConnectionState only rejects entries in pendingRequests with the new typed TerminalHostClientDisposedError. sendRequestOnStream and the connection waiters in ensureConnected use separate promises that aren't covered here, so if disconnect() fires during stream auth or connection setup, callers can still see generic timeout/connection-failure errors that bypass the TerminalHostClientDisposedError mapping in createOrAttach and continue to surface as 500s.</comment>

<file context>
@@ -802,7 +816,7 @@ export class TerminalHostClient extends EventEmitter {
 		for (const [id, pending] of this.pendingRequests.entries()) {
 			clearTimeout(pending.timeoutId);
-			pending.reject(new Error("Connection lost"));
+			pending.reject(pendingRequestError ?? new Error("Connection lost"));
 			this.pendingRequests.delete(id);
 		}
</file context>

@saddlepaddle

Copy link
Copy Markdown
Collaborator Author

Addressed the greptile/CodeRabbit finding that only createOrAttach translated the disposal error: every other terminal procedure would still have surfaced quit-time teardown as a 500, just with a new message. Replaced the per-procedure arm with a router-level terminalProcedure middleware that maps TerminalHostClientDisposedError to PRECONDITION_FAILED / { kind: "TERMINAL_HOST_CLIENT_DISPOSED" } for all 17 procedures; it runs inside the Sentry middleware, so nothing reaches it as a 500. Unexpected socket drops still reject with the generic "Connection lost" and still report.

@saddlepaddle
saddlepaddle merged commit d0ca297 into main Aug 5, 2026
11 of 12 checks passed
@github-actions

github-actions Bot commented Aug 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! 🎉

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/lib/trpc/routers/terminal/terminal.ts`:
- Line 237: The terminal handlers around terminalProcedure must preserve
TerminalHostClientDisposedError from terminal.write(input) and terminal.kill()
operations. Re-throw that exact typed error before generic write error handling,
and inspect Promise.allSettled results in the bulk kill handlers so disposal
rejections propagate as typed failures instead of being swallowed.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a3762c4-188c-44ec-b98f-b77a20a61c3e

📥 Commits

Reviewing files that changed from the base of the PR and between 8ed37ff and fb156a3.

📒 Files selected for processing (1)
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.ts

}),

write: publicProcedure
write: terminalProcedure

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  --glob '*.ts' \
  --glob '*.tsx' \
  'TerminalHostClientDisposedError|terminal\.write|terminal\.kill|killDaemonSessionsForWorkspace|Promise\.allSettled' \
  apps/desktop/src

Repository: superset-sh/superset

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- terminal router slice ---'
sed -n '1,110p;220,275p;300,430p' apps/desktop/src/lib/trpc/routers/terminal/terminal.ts

echo '--- terminal host client disposal slice ---'
sed -n '150,162p;1630,1700p' apps/desktop/src/main/lib/terminal-host/client.ts

echo '--- methods mentioning pendingRequestError or request error setup ---'
rg -n -C 4 'pendingRequestError|uniqueRequestId|requestError|call|send|once|emit|request|write|kill' apps/desktop/src/main/lib/terminal-host/client.ts | head -n 260

Repository: superset-sh/superset

Length of output: 19093


Preserve typed terminal disposal rejections in these handlers.

terminalProcedure only preserves TerminalHostClientDisposedError from next(). terminal.write(input) can reject synchronously from the client’s pending request cleanup, so the generic try/catch hides the original error. Re-throw that exact error before the generic write handling. The bulk kill handlers also swallow rejected terminal.kill() calls in Promise.allSettled instead of treating disposal as a typed failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/lib/trpc/routers/terminal/terminal.ts` at line 237, The
terminal handlers around terminalProcedure must preserve
TerminalHostClientDisposedError from terminal.write(input) and terminal.kill()
operations. Re-throw that exact typed error before generic write error handling,
and inspect Promise.allSettled results in the bulk kill handlers so disposal
rejections propagate as typed failures instead of being swallowed.

@saddlepaddle

Copy link
Copy Markdown
Collaborator Author

Follow-up on the latest CodeRabbit note: the write half is valid and now fixed — that handler converts any caught error to INTERNAL_SERVER_ERROR, which the router middleware can't reclassify, so it rethrows TerminalHostClientDisposedError untouched first.

Skipping the bulk-kill half: killAllDaemonSessions / killDaemonSessionsForWorkspace collect results with Promise.allSettled and never rethrow, so a disposal rejection there produces no error response at all — nothing reaches the Sentry middleware, which is the behaviour this PR is after. Turning those into typed failures would be a UX change, not a reporting fix.

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