What
Quitting or crashing Programa kills every terminal child. Long unattended agent runs are the main thing people use Programa for, so this is the biggest gap between what the app is for and what it does.
Depends on #181 (durable state), which supplies the journal this reattaches to.
Approach: escrow the dup, do not move custody
Ghostty keeps creating and owning every PTY exactly as it does now, so the spawn path and the typing hot path are untouched. At PTY creation the app dups the master fd and sends the dup once over SCM_RIGHTS to programad, along with the child PID, a session UUID, and a capability token.
When the app dies, the kernel closes its copy but the escrowed dup keeps the pair open, so the child never gets SIGHUP and keeps running. programad notices the app is gone and starts draining the master into the WAL so the child never blocks on a full buffer. On relaunch the app fetches the fd back, replays the WAL tail through Ghostty's own VT parser, and re-applies size plus SIGWINCH.
This was chosen over a full daemon-owned PTY broker because the Ghostty change shrinks from "every spawn" to "revival only".
Validated
The load-bearing kernel assumption is proven on Darwin 25.5 with a two-binary PoC: after the owning process exits without cleanup, the child survives un-HUPped and stays interactive through the escrowed fd. Full write-up in docs/plans/detached-sessions.md section 0.
Scope
- Small read-only accessor in the ghostty fork to expose the PTY master fd
- dup + SCM_RIGHTS escrow to programad at PTY creation, token-gated on retrieval
- App-death detection (kqueue EVFILT_PROC plus a heartbeat backstop) and WAL draining
- Revival entry point in the fork that wires termio onto an existing fd and child pid
session.list / session.attach / session.detach socket methods and programa attach
- Re-escrow on programad reconnect so the daemon's own restart is not a single point of failure
Risks
Death detection latency is the one to watch. If draining starts late the child blocks writing into a full PTY buffer, which is worse than today's clean SIGHUP. Bound it with both an event-driven signal and a heartbeat.
Verify
Start a long agent run, quit Programa, relaunch: the run is still going and the pane reattaches live with scrollback intact. Repeat with kill -9 instead of quit.
What
Quitting or crashing Programa kills every terminal child. Long unattended agent runs are the main thing people use Programa for, so this is the biggest gap between what the app is for and what it does.
Depends on #181 (durable state), which supplies the journal this reattaches to.
Approach: escrow the dup, do not move custody
Ghostty keeps creating and owning every PTY exactly as it does now, so the spawn path and the typing hot path are untouched. At PTY creation the app dups the master fd and sends the dup once over SCM_RIGHTS to programad, along with the child PID, a session UUID, and a capability token.
When the app dies, the kernel closes its copy but the escrowed dup keeps the pair open, so the child never gets SIGHUP and keeps running. programad notices the app is gone and starts draining the master into the WAL so the child never blocks on a full buffer. On relaunch the app fetches the fd back, replays the WAL tail through Ghostty's own VT parser, and re-applies size plus SIGWINCH.
This was chosen over a full daemon-owned PTY broker because the Ghostty change shrinks from "every spawn" to "revival only".
Validated
The load-bearing kernel assumption is proven on Darwin 25.5 with a two-binary PoC: after the owning process exits without cleanup, the child survives un-HUPped and stays interactive through the escrowed fd. Full write-up in
docs/plans/detached-sessions.mdsection 0.Scope
session.list/session.attach/session.detachsocket methods andprograma attachRisks
Death detection latency is the one to watch. If draining starts late the child blocks writing into a full PTY buffer, which is worse than today's clean SIGHUP. Bound it with both an event-driven signal and a heartbeat.
Verify
Start a long agent run, quit Programa, relaunch: the run is still going and the pane reattaches live with scrollback intact. Repeat with
kill -9instead of quit.