feat: add opt-in local tmux persistence

Merged
AU
2mo ago
+5003-105

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 (plus cmux tmux attach alias).
  • A per-user tmux server/socket under ~/.cmux/local-tmux owns 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 validated TMUX= CMUX_LOCAL_TMUX=1 attach 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 -parse on all new and touched Swift files.
  • Standalone registry/tmux lifecycle harness: detached start → list/status → close.
  • bash scripts/check-pbxproj.sh
  • bash scripts/lint-pbxproj-test-wiring.sh --repo-root .
  • python3 scripts/check-workspace-package-groups.py --check
  • python3 scripts/check-package-resolved-policy.py
  • git 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 close explicitly.
  • 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-tmux and the tmux alias.
    • 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.
  • 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-9cafb3ef7e37 compiled current HEAD 3ec33dfab3 remotely in 270 seconds with CMUX_DEV_BACKEND_MODE=off; no local xcodebuild was run.
  • Hosted Blacksmith runs 33873845754, 33873845756, and 33873845618 each executed one selected local-tmux regression at 908bbd80dd; the latest hosted E2E runs 33905531813, 33905531893, and 33905531908 each executed one selected regression at 95ac7807f1. The local-tmux feature code is unchanged across those merge-forward commits.
  • Real tagged CLI dogfood created a session under hostile exit-unattached on, observed exit-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 6e94cdb46f and retained through merge 3ec33dfab3 by removing duplicate workspace polling. On the current tagged app, local-tmux attach --workspace workspace:1 --focus=false --json returned surface_ref=surface:3 and exit 0 through the real socket.
  • Mainline's Xcode 26.3 type-check failure in CLI/cmux.swift:5847 was repaired in 908bbd80dd; 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.