Skip to content

fix(tmux): isolate sessions on a dedicated socket + raise pane nofile limit (fixes new-session crash after tmux upgrade)#96

Merged
Ark0N merged 2 commits into
Ark0N:masterfrom
TeigenZhang:fix/tmux-single-socket
May 25, 2026
Merged

fix(tmux): isolate sessions on a dedicated socket + raise pane nofile limit (fixes new-session crash after tmux upgrade)#96
Ark0N merged 2 commits into
Ark0N:masterfrom
TeigenZhang:fix/tmux-single-socket

Conversation

@TeigenZhang
Copy link
Copy Markdown
Contributor

Symptom

After upgrading tmux to the latest version, opening a new conversation immediately errored out — the freshly spawned CLI process exited right away, so no session could start.

Root cause

Under macOS launchd, the newer tmux hands panes a low soft nofile limit (256). Recent Claude Code exits immediately when it inherits such a low file-descriptor limit, so every newly spawned session died on startup.

Separately, Codeman's sessions lived on the default tmux server intermixed with the user's own tmux sessions, and carried a per-session tmuxSocket tag that could drift from physical reality — causing live sessions to be wrongly marked dead ("tab shows no session found") and duplicate "Restored:" tabs.

Fix

  1. Raise the pane nofile limit (ulimit -Sn) before launching the CLI, so the new tmux + launchd combo no longer crashes Claude Code on spawn.
  2. Isolate all Codeman sessions onto a dedicated socket (tmux -L codeman), fully separated from the user's default tmux server.
  3. Unify to a single socket — remove the drift-prone per-session tmuxSocket field. reconcileSessions() collapses from a multi-socket scan to a single list-panes query; loadSessions() strips the obsolete field. Socket name exposed as TmuxManager.muxSocket, overridable via CODEMAN_TMUX_SOCKET.
  4. Route two sibling bare-tmux call sites through the socket (same regression class): queryTmuxWindowSize() (was silently falling back to 120×40 on re-attach, losing scrollback) and the send-key route (Shift+Enter / Ctrl+Enter newline).
  5. SSH chooser scripts (tmux-manager.sh, tmux-chooser.sh) route every tmux call through tmux -L $CODEMAN_TMUX_SOCKET.

Test plan

  • tsc --noEmit clean
  • npm test -- test/tmux-manager.test.ts test/tmux-window-size-query.test.ts → 46 passing
  • Verified on a live deployment: new sessions spawn without the nofile crash; existing tabs reconcile on the dedicated socket; no "session not found" or duplicate "Restored:" tabs.

🤖 Generated with Claude Code

Teigen added 2 commits May 25, 2026 20:14
Remove the per-session `tmuxSocket` field that recorded which tmux server
each session lived on (default vs the `codeman` socket). That field was a
persisted cache of physical reality and could drift — causing live sessions
to be wrongly marked dead ("tab shows no session found") and spawning
duplicate "Restored:" tabs.

All Codeman sessions now live on one process-wide socket (`tmux -L codeman`,
overridable via CODEMAN_TMUX_SOCKET), exposed via TmuxManager.muxSocket on
the TerminalMultiplexer interface. reconcileSessions() collapses from a
multi-socket scan (locate / re-pin / cross-socket dedup) to a single
`list-panes` query. loadSessions() strips the obsolete field from on-disk
records so it stops being written back.

Also fix two sibling bare-`tmux` call sites the unification would otherwise
leave broken (same Ark0N#80 regression class — bare tmux hits the user's default
server and never finds a session on the codeman socket):
- session.ts queryTmuxWindowSize(): add `-L <socket>` (was silently falling
  back to 120x40 on re-attach, losing scrollback)
- session-routes.ts send-key (Shift+Enter / Ctrl+Enter newline): route
  through ctx.mux.muxSocket

SSH chooser scripts (tmux-manager.sh, tmux-chooser.sh) route every tmux call
through `tmux -L $CODEMAN_TMUX_SOCKET`, matching the TS default.
@Ark0N
Copy link
Copy Markdown
Owner

Ark0N commented May 25, 2026

Thank you very much for this — it's an excellent, thorough fix. 🙏

I reviewed the full diff and it's in great shape:

  • nofile fix (buildNofileLimitCommand) is prepended to both the spawn and respawn command strings — correct coverage of both paths.
  • Single-socket isolation is threaded consistently through every tmux call site (manager, queryTmuxWindowSize, the send-key route, and both shell chooser scripts), with the socket name validated (SAFE_TMUX_SOCKET_PATTERN) and shell-escaped — injection-safe.
  • Dropping the per-session tmuxSocket field plus the loadSessions() dedup-by-muxName pass cleanly retires the drift that caused the phantom "Restored:" twins.
  • Tests are great — the hex-allowlist negative case and the -L <socket>-must-lead call-shape assertions are exactly the right regression guards. CI is green.

One tiny non-blocking nit for a future pass: the buildNofileLimitCommand JSDoc block is in Chinese while the rest of the file is English — worth translating for consistency.

Merging now. Thanks again for the careful work and detailed write-up!

@Ark0N Ark0N merged commit 1ff315a into Ark0N:master May 25, 2026
1 check passed
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