JetBrains / JetBrains/thinkrail

Terminal freezes after macOS sleep: a lost WebSocket drain event latches backpressure forever

Open
#428 3 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
TypeScript
Stars
467
Forks
37
Avg merge
1d 56m
Merged PRs (30d)
141

Description

## Symptom

After a macOS hibernate (in my case combined with a monitor/App move), a terminal tab stops updating entirely while the process underneath keeps running — Claude Code kept working in the pty, but the pane showed a frozen frame. The WebSocket between webview and host was still `ESTABLISHED` (verified with `lsof`), so it isn't a dead connection. Reloading the window recovers, which is why this hides well.

## Root cause

Terminal output flow control is edge-triggered with no recovery path:

1. During sleep the webview stops reading its socket, a `ws.send` on the host backpressures, and the host latches the client in `terminalBackpressured` while the tab's output batcher sets `blocked`.
2. The **only** thing that lifts the latch is Bun's `drain` event (`packages/server/src/host/server.ts`). The batcher deliberately retries only on `resume()` — that's a pinned semantic — so the latch's lift must be guaranteed.
3. A drain lost across a system sleep is never re-delivered. The latch stays set forever: every later flush returns "unavailable", the tab freezes, the pty and the output recorder stay live underneath.

## Fix

Don't trust the drain event alone — the socket itself knows the truth. A 1-second reconciler checks each latched client: `getBufferedAmount() === 0` with the flag still set means the OS never delivered the drain, so the host fires it (clears the flag, resumes the client's terminals). Real backpressure is untouched — a socket with buffered bytes stays latched until drain or the reconciler sees it empty.

Fix on my branch:

CommanderTvis/thinkrail@efa2f00de5b30938c5b50ba5db02c6426df9ff29

Happy to send it as a standalone PR.

---
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Contributor guide

Open the contributing guide

Research direction

Start in packages/server/src/host/server.ts and trace the terminalBackpressured latch, the output batcher's blocked state, and the WebSocket drain handling. Reproduce the sleep-related freeze if possible, then verify that a latched socket whose getBufferedAmount() is zero resumes terminal output without changing behavior during real backpressure.

Written by the indexing model from the issue text.

Assessment

Tech stack
bun, typescript
Domain
api, backend, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.