feat: add opt-in local tmux persistence
Closes #10651
What landed
This is the first coherent tmux-backed vertical slice for local persistence:
cmux local-tmux start|attach|list|status|detach|close|cleanup(pluscmux tmux attachalias).- A per-user tmux server/socket under
~/.cmux/local-tmuxowns opted-in shells, agents, dev servers, PTYs, and bounded scrollback independently of the GUI process. - A stable logical session UUID registry stores the tmux identity and workspace title/cwd hints. Runtime cmux IDs are treated as hints, not the reattach key.
- GUI attachment uses the existing authenticated control socket and
surface.create/surface.respawn, persisting a narrowly validatedTMUX= CMUX_LOCAL_TMUX=1attach command through the existing session snapshot restore path. - Local tmux ownership takes precedence over agent resume metadata during both main-workspace and Dock restore, so an agent inside the durable server is not launched a second time.
- Headless
attach --headless, safe multi-client--new-client, fail-closed multi-client detach, secure 0700/0600 state, and stale-record cleanup are included.
The ordinary local terminal process model is unchanged unless this explicit profile is selected.
Verification
swift test --package-path Packages/macOS/CmuxWorkspaces --filter WorkspaceSessionRestorePolicyServiceTests/restorableTmuxStartCommandAcceptsLocalTmuxMarker(passes; the architecture-specific bundle warning is from this host's test runner).swiftc -parseon all new and touched Swift files.- Standalone registry/tmux lifecycle harness: detached start → list/status → close.
bash scripts/check-pbxproj.shbash scripts/lint-pbxproj-test-wiring.sh --repo-root .python3 scripts/check-workspace-package-groups.py --checkpython3 scripts/check-package-resolved-policy.pygit diff --check
The repository checkout does not contain the requested scripts/swift_file_length_budget.py or .github/swift-file-length-budget.tsv; the new Swift files were kept below 500 lines and the available external budget check showed no new file over the threshold.
Follow-up / limitations
- The CLI is the documented API in this slice; a first-class
local_tmux.*socket namespace and richer persistent workspace metadata can follow without changing the default PTY path. - Closing a cmux surface does not kill the durable tmux owner; use
local-tmux closeexplicitly. - Applications that require an outer GUI-specific PTY/device need their own reconnect behavior. See
docs/local-tmux.md.
A tagged cloud build and launch were performed; the final runtime limitation is recorded in the verification section below. The current HEAD and hosted validation are recorded in the closeout section.
Summary by CodeRabbit
-
New Features
- Added opt-in local tmux session management through
cmux local-tmuxand thetmuxalias. - Supports starting, attaching, listing, inspecting, detaching, closing, and cleaning up sessions.
- Added persistent sessions, headless operation, JSON output, and workspace-aware restoration.
- Added validation and safeguards for session names, paths, permissions, and stale sessions.
- Added opt-in local tmux session management through
-
Documentation
- Added CLI usage, safety guidance, limitations, and local tmux workflow documentation.
- Added English and Japanese command-line help text.
-
Bug Fixes
- Local tmux sessions now take precedence during terminal restoration.
Persistence boundary and alternatives
This profile deliberately owns only an opted-in local tmux session. The tmux server owns the shell, agent/dev-server processes, PTY, and bounded (10,000-line) scrollback; cmux owns a client surface and a secure registry binding. The profile forces exit-unattached off before detached creation, so cmux Cmd-Q, a crash, an app update, or closing a surface leaves the owner running and local-tmux attach can validate and reconnect it. Normal lid-close/sleep usually preserves the process image, subject to macOS and the child application's own network behavior.
A local process cannot run while the Mac is logged out, shut down, rebooted, or without power. In those cases the live tmux server, children, PTY state, and in-memory scrollback are gone; the on-disk registry may remain, but its memory-only server-incarnation marker makes reattach fail closed rather than attach to a replacement. This slice intentionally does not pretend to resurrect commands after boot. If exact continuity while the laptop is offline is required, put the owner on a remote host with cmux ssh-tmux, cmux mosh-tmux, or a persistent cloud VM. tmux-resurrect/continuum, zellij serialization, or launchd can recreate selected commands/layouts after login, but cannot restore live process memory, SSH state, or exact scrollback.
Final verification
- Tagged cloud build
issue-10651-local-tmux-daemon-9cafb3ef7e37compiled current HEAD3ec33dfab3remotely in 270 seconds withCMUX_DEV_BACKEND_MODE=off; no local xcodebuild was run. - Hosted Blacksmith runs
33873845754,33873845756, and33873845618each executed one selected local-tmux regression at908bbd80dd; the latest hosted E2E runs33905531813,33905531893, and33905531908each executed one selected regression at95ac7807f1. The local-tmux feature code is unchanged across those merge-forward commits. - Real tagged CLI dogfood created a session under hostile
exit-unattached on, observedexit-unattached off, terminated the tagged cmux app, and successfully listed, inspected, and explicitly closed the still-live session afterward. - The explicit workspace attach path was fixed in
6e94cdb46fand retained through merge3ec33dfab3by removing duplicate workspace polling. On the current tagged app,local-tmux attach --workspace workspace:1 --focus=false --jsonreturnedsurface_ref=surface:3and exit 0 through the real socket. - Mainline's Xcode 26.3 type-check failure in
CLI/cmux.swift:5847was repaired in908bbd80dd; the later normal merge-forward retains current mainline Cloud/Surface changes. - Static validation, including Swift parsing, package/project checks, test wiring, and
git diff --check, passed without changing the warning TSV. - README session-restore wording distinguishes reconstructed app state from live local-tmux ownership and points machine-off continuity to remote tmux/cloud owners.
Current closeout state
At current HEAD 3ec33dfab39f9e46cd9a440f954a0243450f1538, fetched origin/main c146ba98df4bab0b3a3fae19488ff5da15c32f85 remains an ancestor after the latest normal merge-forward. All required contexts and both Vercel previews are green; GitHub reports MERGEABLE with mergeStateStatus=BLOCKED because branch protection requires one approving review and reviewDecision is empty. The review graph has 46/46 inline threads replied to and resolved, with no new actionable activity after the push. The current tagged build compiled successfully; real tmux lifecycle and explicit workspace GUI surface creation both passed on the tagged app. The canonical audit table is comment 5509514104, rechecked against this HEAD. The PR is intentionally not merged because one maintainer approval remains outstanding. A local tmux owner cannot preserve live processes through logout, restart, shutdown, or power loss; machine-off continuity requires a remote tmux/cloud owner or a separate resurrection feature. Next action: obtain one maintainer approval.