Skip to content

fix(terminal): reattach recovered daemon sessions on relaunch + tmux persistence/stability batch#81

Merged
openwong2kim merged 4 commits into
mainfrom
fix/daemon-reattach-and-stability
May 30, 2026
Merged

fix(terminal): reattach recovered daemon sessions on relaunch + tmux persistence/stability batch#81
openwong2kim merged 4 commits into
mainfrom
fix/daemon-reattach-and-stability

Conversation

@openwong2kim
Copy link
Copy Markdown
Owner

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 start showed blank terminals with no scrollback. Persistence, daemon
reconnect, 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:

if (daemonModeAtMount) {
  reconnectPtyWithRetry(ptyId, () => terminalRef.current === terminal);
}

reconnectPtyWithRetry checks its isCurrent guard synchronously on the
first line (if (!isCurrent()) return). But that call ran before the same
effect's later terminalRef.current = terminal assignment, so on every fresh
mount the guard was false, the retry bailed immediately, and pty.reconnect
was never called
. The daemon never attached a SessionPipe to the recovered
session → no RingBuffer replay → blank pane. pty.create still worked, which
is why only restored sessions went blank.

Fix: 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.

What's in here

fix(terminal) — blank-terminal fix (commit 2)

  • Dedicated daemon-reattach effect in useTerminal (the real fix).
  • Process-wide WebGL context LRU pool (cap 12) so restoring many sessions can't
    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-kill
    daemon, R2 session-host process for live-process survival across daemon death).

feat(persistence) — tmux-style detach + dogfood batch (commit 1)

  • Quit now detaches (daemon + live sessions survive, next launch reattaches);
    only the tray "Shut down wmux" tears the daemon down, with a pid-kill backstop.
  • Drop the per-pane token chip (TokenTracker/tokenSlice), Ctrl+arrow pane
    navigation + cheat-sheet entries, right-click copy that keeps the selection,
    RSS memory reporting, completed-agent border blink.

Verification

  • GUI dogfood: terminals now show their restored scrollback after a daemon
    restart (user-confirmed). Daemon log shows attachSession + a 104 KB
    SessionPipe.flush per recovered session.
  • Tests: 2047 passing (164 files) + tsc clean. New source-level regression
    lock useTerminal.daemonReattach.test.ts (reattach must run after terminalRef
    is set, never with the assigned-later === terminal guard) + 8-test WebGL pool
    unit suite + the detach invariants in beforeQuitDisconnectRace / shutdownRpc.

Notes / follow-ups (not in this PR)

  • A handful of WebGL "too many active contexts" warnings still appear during the
    startup burst (GPU context disposal lags JS disposal). Cosmetic — terminals are
    not blank (DOM-renderer fallback). Tunable later.
  • R1 (daemon must not self-kill a wedged-but-live daemon) and R2 (session-host
    process) are scoped in the design doc, not implemented here.

🤖 Generated with Claude Code

iamwongeeeee and others added 4 commits May 30, 2026 19:43
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 openwong2kim merged commit 3bda8bc into main May 30, 2026
4 checks passed
@openwong2kim openwong2kim deleted the fix/daemon-reattach-and-stability branch May 30, 2026 11:20
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>
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.

2 participants