Skip to content

fix: prevent tmux detach from stalling Happy CLI#539

Open
dzlobin wants to merge 1 commit intoslopus:mainfrom
dzlobin:fix/533-tmux-detach-stall
Open

fix: prevent tmux detach from stalling Happy CLI#539
dzlobin wants to merge 1 commit intoslopus:mainfrom
dzlobin:fix/533-tmux-detach-stall

Conversation

@dzlobin
Copy link

@dzlobin dzlobin commented Feb 4, 2026

Summary

  • Wraps Ink's stdout in a non-blocking proxy that drops writes when PTY backpressure is detected (e.g. tmux detach), preventing the Node.js event loop from blocking
  • Applied consistently across Claude, Codex, and Gemini agent launchers
  • Includes 12 unit tests covering normal writes, backpressure detection, drain recovery, and listener accumulation prevention

Root Cause

When tmux detaches, the PTY buffer (~4KB on macOS) fills with no reader. process.stdout.write() then blocks synchronously, freezing the entire Node.js event loop and stalling all Happy instances in that tmux session.

Fix

A Proxy wraps process.stdout and intercepts write() calls. When writableNeedDrain is true or write() returns false, subsequent writes are silently dropped (with callbacks still invoked) until a drain event fires. This is passed to Ink's render() via the stdout option.

Closes #533

Test plan

  • 12 unit tests passing (vitest)
  • Manual testing: run happy in tmux, detach, verify no stall, reattach and confirm UI resumes
  • Test with multiple instances in same tmux session

🤖 Generated with Claude Code

When tmux detaches, the PTY buffer fills and process.stdout.write()
blocks synchronously, freezing the Node.js event loop. This wraps
Ink's stdout in a non-blocking proxy that drops writes during
backpressure instead of blocking.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <[email protected]>
Co-Authored-By: Happy <[email protected]>
HirokiKobayashi-R added a commit to HirokiKobayashi-R/happy that referenced this pull request Feb 11, 2026
Cherry-picked from upstream PRs:
- slopus#566: Path normalization alignment with Claude Code (Adam Murphy)
- slopus#561: YOLO mode race condition fix (Adam Murphy)
- slopus#570: Terminal state restoration after remote→local switch (blechschmidt/tomstetson)
- slopus#539: tmux detach stalling prevention (claudio)

All 4 PRs applied successfully. slopus#561 required conflict resolution
(merged effectiveMode race fix with AskUserQuestion guard).
@HirokiKobayashi-R
Copy link

We've been testing a similar fix in our fork for about a week across multiple tmux setups (both detach/reattach cycles and long-running detached sessions), and can confirm this approach resolves #533 effectively. The Proxy-based wrapper is a clean solution — it avoids patching process.stdout globally and keeps the non-blocking behavior scoped to where Ink actually needs it.

A few observations from our testing:

  1. The writableNeedDrain pre-check (line 44) is important. Without it, the first write after detach still goes through before write() returns false, which in our experience sometimes caused a brief freeze before dropping kicks in. Checking writableNeedDrain first catches the backpressure state proactively.

  2. The once('drain') listener deduplication (lines 56-62) is well-handled. We initially missed the guard (if (!dropping)) and ended up stacking drain listeners on repeated writes during backpressure. The test at line 195 (should not accumulate drain listeners) validates exactly this case — good coverage.

  3. One edge case worth considering: when tmux reattaches, the drain event fires and writes resume, but if there's a burst of queued Ink renders, they all hit stdout at once. In practice we haven't seen this cause issues (Ink's internal batching seems to handle it), but it might be worth a note in the comments about why this is safe.

  4. Coverage across all launchers: The PR applies the wrapper to claudeRemoteLauncher, runCodex, and runGemini — this matches all the Ink render sites. We found that missing even one launcher meant that specific mode would still freeze in tmux.

Solid fix. Hope this gets merged soon — it's a real pain point for tmux users running multiple sessions.

leeroybrun added a commit to happier-dev/happier that referenced this pull request Feb 15, 2026
@happier-bot

This comment was marked as abuse.

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.

Only one happy instance works reliably when detached in tmux

3 participants