macOS: keep pane programs running across daemon restarts (an FD store without systemd) #18

Closed
opened 2026-10-02 01:32:17 +00:00 by jhgaylor · 1 comment
Owner

On Linux, restarting or upgrading illogicald leaves every pane's program running (M2b): each pane runs in its own systemd scope, and its PTY master waits in systemd's FD store until the new daemon takes it back. macOS has neither, so on a Mac a daemon restart ends every program running in a pane.

What happens today (seen on jake-mini, 2026-10-01)

launchctl kickstart -k gui/$UID/illogicald with two panes: a shell that had run cd /tmp && echo before-restart-42, and a pane running sleep 600.

  • The daemon saved both panes and exited cleanly; launchd started the new one within a second.
  • Layout, pane ids, the session and scrollback came back, each pane marked ── restored <time> ──.
  • Both programs were gone. The shell pane got a new zsh in its last directory. The sleep 600 pane got a fresh shell (default policy); the sleep was not rerun.

That's the restart policy doing its job, but it's the pre-M2b experience: any upgrade or crash on the Mac costs you every running shell, editor, build and agent.

Why

Nothing keeps a pane's PTY master open while the daemon is gone. When the daemon exits, the master closes, the terminal hangs up, and the programs on it get SIGHUP. launchd has no FD store, and the daemon starts panes without scopes there (Launcher { scopes: false, fd_store: false }).

Sketch: a holder that outlives the daemon

Something other than the daemon must own the masters across a restart, and hand them back.

  • Option A, per-pane: the shim (illogicald _shim) already outlives the daemon and records the program's exit. Let it also keep a copy of the master, and listen on a per-pane Unix socket in the state dir. A new daemon connects and receives the master with SCM_RIGHTS, like LISTEN_FDS today. The cost is one small process per pane, which already exists.
  • Option B, one holder per host: a tiny illogicald _holder started by the daemon (or as its own launchd agent) that keeps all masters and hands them over on request. That's fewer processes, but one more thing to supervise and upgrade.
  • Either way:
    • The shim and the program must leave the daemon's launchd job. Check AbandonProcessGroup and the job's kill semantics, so kickstart -k and a crash don't take them down. Run them in their own session (they already setsid).
    • stop (logout, launchctl bootout) should still end the panes, as systemctl --user stop does on Linux; only restart and upgrade keep them.
    • Adoption reuses M2b's path: the shim's record says the pid and start time, then rebuild the VT from checkpoint and log tail, and read the gap's output from the master.
  • The same mechanism would help Linux hosts without systemd (sandboxes, containers), which also lose panes on a daemon restart today.

Done when

  • On a Mac, launchctl kickstart -k gui/$UID/illogicald (and installing a new build) leaves vim and a running build untouched, and clients reconnect on their own, as M2b's done-when says for Linux.
  • A daemon crash (kill -9) on a Mac also keeps the panes' programs running.
  • launchctl bootout (and logging out) still ends them.
  • The README's Mac section no longer warns that restarts end panes.
On Linux, restarting or upgrading `illogicald` leaves every pane's program running (M2b): each pane runs in its own systemd scope, and its PTY master waits in systemd's FD store until the new daemon takes it back. macOS has neither, so on a Mac a daemon restart ends every program running in a pane. ### What happens today (seen on jake-mini, 2026-10-01) `launchctl kickstart -k gui/$UID/illogicald` with two panes: a shell that had run `cd /tmp && echo before-restart-42`, and a pane running `sleep 600`. - The daemon saved both panes and exited cleanly; launchd started the new one within a second. - Layout, pane ids, the session and scrollback came back, each pane marked `── restored <time> ──`. - **Both programs were gone.** The shell pane got a new zsh in its last directory. The `sleep 600` pane got a fresh shell (default policy); the sleep was not rerun. That's the restart policy doing its job, but it's the pre-M2b experience: any upgrade or crash on the Mac costs you every running shell, editor, build and agent. ### Why Nothing keeps a pane's PTY master open while the daemon is gone. When the daemon exits, the master closes, the terminal hangs up, and the programs on it get SIGHUP. launchd has no FD store, and the daemon starts panes without scopes there (`Launcher { scopes: false, fd_store: false }`). ### Sketch: a holder that outlives the daemon Something other than the daemon must own the masters across a restart, and hand them back. - **Option A, per-pane:** the shim (`illogicald _shim`) already outlives the daemon and records the program's exit. Let it also keep a copy of the master, and listen on a per-pane Unix socket in the state dir. A new daemon connects and receives the master with `SCM_RIGHTS`, like `LISTEN_FDS` today. The cost is one small process per pane, which already exists. - **Option B, one holder per host:** a tiny `illogicald _holder` started by the daemon (or as its own launchd agent) that keeps all masters and hands them over on request. That's fewer processes, but one more thing to supervise and upgrade. - Either way: - The shim and the program must leave the daemon's launchd job. Check `AbandonProcessGroup` and the job's kill semantics, so `kickstart -k` and a crash don't take them down. Run them in their own session (they already `setsid`). - `stop` (logout, `launchctl bootout`) should still end the panes, as `systemctl --user stop` does on Linux; only restart and upgrade keep them. - Adoption reuses M2b's path: the shim's record says the pid and start time, then rebuild the VT from checkpoint and log tail, and read the gap's output from the master. - The same mechanism would help Linux hosts without systemd (sandboxes, containers), which also lose panes on a daemon restart today. ### Done when - On a Mac, `launchctl kickstart -k gui/$UID/illogicald` (and installing a new build) leaves vim and a running build untouched, and clients reconnect on their own, as M2b's done-when says for Linux. - A daemon crash (`kill -9`) on a Mac also keeps the panes' programs running. - `launchctl bootout` (and logging out) still ends them. - The README's Mac section no longer warns that restarts end panes.
Author
Owner

Done in e9704b5 (the merge of bc6a443) and rolled out to jake-mini. There, launchctl kickstart -k, kill -9 and illogicald install (an upgrade) keep pane programs running with no lost output, and launchctl bootout ends them after a minute. Linux hosts without systemd get the same with --keep-panes.

Done in e9704b5 (the merge of bc6a443) and rolled out to jake-mini. There, `launchctl kickstart -k`, `kill -9` and `illogicald install` (an upgrade) keep pane programs running with no lost output, and `launchctl bootout` ends them after a minute. Linux hosts without systemd get the same with `--keep-panes`.
Sign in to join this conversation.
No description provided.