JetBrains / JetBrains/thinkrail
Terminal freezes after macOS sleep: a lost WebSocket drain event latches backpressure forever
- 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
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