fix(terminal): reattach recovered daemon sessions on relaunch + tmux persistence/stability batch#81
Merged
Conversation
Quit now DETACHES from the daemon — every live PTY session keeps running and the next launch reattaches — instead of shutting the daemon down. Only the explicit tray "Shut down wmux (close all sessions)" tears the daemon down, with a verified pid-kill backstop if the graceful daemon.shutdown RPC times out. The daemon-side shutdown handler force-exits within 1s and delays its socket teardown so the ack flushes (orphan-daemon + ack-miss fixes). Locked by beforeQuitDisconnectRace + shutdownRpc source invariants. Also folds in the dogfood stability batch verified alongside it: - Drop the per-pane token chip and its plumbing (TokenTracker, tokenSlice). - Ctrl+arrow pane navigation + matching keyboard cheat-sheet entries. - Right-click copy keeps the selection (kills the copy<->paste collision). - RSS-based memory reporting; completed-agent pane border blink. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…minal fix) The mount-time daemon reattach lived inside the terminal-creation effect and guarded with `() => terminalRef.current === terminal`. reconnectPtyWithRetry evaluates that isCurrent guard SYNCHRONOUSLY on invocation, but the call ran BEFORE the same effect's later `terminalRef.current = terminal` assignment — so on every fresh mount the guard was false, the retry bailed at its first `if (!isCurrent()) return`, and pty.reconnect was NEVER called. The daemon then never attached a SessionPipe to a recovered session: no RingBuffer replay, blank terminal. (Dogfood: 20 live daemon sessions, zero daemon-side attachSession.) pty.create still worked, which is why only restored sessions went blank. Move the reattach into a dedicated effect that runs after the mount effect (so terminalRef is set), fire it when daemon mode is active at mount OR on a later daemon:connected (fresh-daemon-spawn startup race / mid-session respawn), and guard with `terminalRef.current !== null`. Verified end to end: daemon attachSession + 104KB SessionPipe scrollback flush + GUI dogfood. Regression locked by useTerminal.daemonReattach source invariants. Also adds a process-wide WebGL context LRU pool (cap 12) so restoring many sessions can't exceed Chromium's ~16-context limit and blank panes via forced eviction — over-budget terminals fall back to xterm's DOM renderer instead. Plus plans/persistence-beyond-tmux.md documenting the longer-term roadmap (R1 never-self-kill daemon, R2 session-host process for live-process survival). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ected (codex P2/P3) Codex review of #81 surfaced two issues, both fixed here: - [P2] The tray "Shut down wmux (close all sessions)" teardown was nested under `clientAtQuit?.isConnected`. When main had already dropped to local-only mode (daemon disconnect / respawn budget exhausted) while daemon.pid still pointed at a live daemon, the explicit close-all fell through to local PTY cleanup and never called killDaemonByPidFile() — leaving the daemon and its PTYs running. Now the local-mode branch also pid-kills the daemon when fullShutdownRequested (verify-before-kill, so a recycled PID is never signalled). A normal Quit still leaves any such daemon alone — that is the persistence promise. Locked by a new beforeQuitDisconnectRace source invariant. - [P3] scripts/persistence-dynamic.mjs used import.meta.dirname (Node 20.11+) while package.json declares Node >=18; derive the script dir from fileURLToPath(import.meta.url) so it runs on the supported engine range. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pawn (codex P2) Re-review of the Fix D reattach effect found two ways it could miss the only reattach opportunity and leave a recovered pane blank until remount: - It gated the daemon:connected handler on isDaemonModeActive(). The terminal's own listener can run before AppLayout's listener flips that module flag to true, so the event was consumed without ever calling pty.reconnect. - It latched with `attached = true` after the first attach, so a mid-session daemon respawn (a new daemon generation, fresh daemon:connected) was ignored and the pane stayed bound to the dead session. Reattach now reads the module flag ONLY for the at-mount case (no event to ride) and reconnects unconditionally on every daemon:connected. A transient in-flight guard (local to the ptyId, not a permanent latch) prevents only the concurrent active-at-mount + daemon:connected double-replay that would duplicate scrollback. Regression assertions extended in useTerminal.daemonReattach. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
openwong2kim
pushed a commit
that referenced
this pull request
May 30, 2026
…iline-paste fix, stability batch Bundles #81 (tmux-style Quit=detach, recovered-session reattach fix, Ctrl+Shift+Arrow pane nav, completion blink, RAM RSS, token-chip removal, WebGL LRU pool) and #84 (multiline paste LF + paste-injection guard) -- everything merged since v2.15.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the blank-terminal-on-relaunch bug the user hit in dogfood: after the
daemon restarts (reboot / crash / old-app shutdown) and recovers its sessions,
npm startshowed blank terminals with no scrollback. Persistence, daemonreconnect, and session recovery were all working — the renderer just never
re-attached to the recovered sessions, so it looked broken.
This PR lands that fix plus the tmux-style persistence work and the dogfood
stability batch that were verified alongside it in the same running app.
Root cause (the blank terminal)
The mount-time daemon reattach lived inside the terminal-creation effect:
reconnectPtyWithRetrychecks itsisCurrentguard synchronously on thefirst line (
if (!isCurrent()) return). But that call ran before the sameeffect's later
terminalRef.current = terminalassignment, so on every freshmount the guard was
false, the retry bailed immediately, andpty.reconnectwas never called. The daemon never attached a
SessionPipeto the recoveredsession → no
RingBufferreplay → blank pane.pty.createstill worked, whichis why only restored sessions went blank.
Fix: move the reattach into a dedicated effect that runs after the mount
effect (so
terminalRefis set), fire it when daemon mode is active at mountor on a later
daemon:connected(fresh-daemon-spawn startup race /mid-session respawn), and guard with
terminalRef.current !== null.What's in here
fix(terminal)— blank-terminal fix (commit 2)useTerminal(the real fix).blow Chromium's ~16-context limit and blank panes via forced eviction;
over-budget terminals fall back to xterm's DOM renderer.
plans/persistence-beyond-tmux.md— persistence roadmap (R1 never-self-killdaemon, R2 session-host process for live-process survival across daemon death).
feat(persistence)— tmux-style detach + dogfood batch (commit 1)only the tray "Shut down wmux" tears the daemon down, with a pid-kill backstop.
navigation + cheat-sheet entries, right-click copy that keeps the selection,
RSS memory reporting, completed-agent border blink.
Verification
restart (user-confirmed). Daemon log shows
attachSession+ a 104 KBSessionPipe.flushper recovered session.tscclean. New source-level regressionlock
useTerminal.daemonReattach.test.ts(reattach must run afterterminalRefis set, never with the assigned-later
=== terminalguard) + 8-test WebGL poolunit suite + the detach invariants in
beforeQuitDisconnectRace/shutdownRpc.Notes / follow-ups (not in this PR)
startup burst (GPU context disposal lags JS disposal). Cosmetic — terminals are
not blank (DOM-renderer fallback). Tunable later.
process) are scoped in the design doc, not implemented here.
🤖 Generated with Claude Code