feat(v0.3): H1 — tmux keepalive (sessions survive a server restart)

- src/session/tmux.ts: sync tmux CLI wrappers (available/has/kill), best-effort
- config: useTmux from USE_TMUX env (1/0/auto→detect tmux on PATH)
- session: when useTmux, spawn 'tmux new-session -A -s web_<id> <shell>' (the
  node-pty proc is a tmux CLIENT); createSession takes an optional id for
  re-attach; Session.tmuxName; kill() runs tmux kill-session
- manager: handleAttach re-attaches to a surviving 'web_<id>' tmux session after
  a restart (Case 3.5); shutdown kills only the client pty for tmux sessions so
  the shell keeps running
- tests: USE_TMUX:0 in shared integration cfg (no tmux leakage); unit CFGs
  useTmux:false; integration 'H1' (real tmux): set var → restart server →
  re-attach → var survived. 208 tests green.
This commit is contained in:
Yaojia Wang
2026-06-17 19:48:39 +02:00
parent af2a0879e3
commit 9099f73534
8 changed files with 198 additions and 9 deletions

View File

@@ -27,6 +27,7 @@ export interface Config {
readonly scrollbackBytes: number; // ring buffer capacity, default 2MB
readonly maxPayloadBytes: number; // max WS frame bytes, default 1MB (L5)
readonly wsPath: string; // WS upgrade path, default '/term' (L3; invariant 8)
readonly useTmux: boolean; // H1: spawn the shell inside tmux so it survives a server restart
readonly allowedOrigins: readonly string[]; // derived from NIC IPs, NOT bindHost (M1)
}
@@ -149,6 +150,8 @@ export interface Session {
exitCode: number | null;
/** Claude Code activity from hooks (H2); updated by manager.handleHookEvent. */
claudeStatus: ClaudeStatus;
/** tmux session name (H1) when running under tmux, else null. */
readonly tmuxName: string | null;
readonly pty: IPty;
}