Skip to content

Detached sessions L2: keep agent processes alive across app quit and crash #182

Description

@arzafran

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

  1. Small read-only accessor in the ghostty fork to expose the PTY master fd
  2. dup + SCM_RIGHTS escrow to programad at PTY creation, token-gated on retrieval
  3. App-death detection (kqueue EVFILT_PROC plus a heartbeat backstop) and WAL draining
  4. Revival entry point in the fork that wires termio onto an existing fd and child pid
  5. session.list / session.attach / session.detach socket methods and programa attach
  6. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions