Skip to content

Agent hand-off: execve upgrade that keeps every shell, local Mac first #230

Closed

Description

@joelmoss

Parent

Part of #154 (remote agent sessions). Phase 3. Design doc docs/designs/remote-workrooms.md, Distribution Plan, "Resume policy versus version lockstep", DECIDED (2026-09-22, owner): hand off.

What to build

A newer agent never kills an older one's sessions, and never leaves the old one running indefinitely either. The outgoing agent hands its sessions to its replacement. The local Mac hands off on app update too (decided 2026-09-22), shipped on Nightly first, because it proves the mechanism on the host that is easiest to debug before the remote bootstrap depends on it.

  • Replace the program in place: execve, not a second process. Pty::wait is a waitpid on the child, which works only for the child's parent. Passing pty masters to a separate process (SCM_RIGHTS) would keep the shells alive but orphan them, losing every exit code. The old agent execves the new binary, so its pid stays the same and it stays every shell's parent. The pty masters and the listening socket survive once close-on-exec is cleared, so the socket is never unbound. The session table (ids, pids, fds, sizes, size owner) crosses in an inherited pipe or memfd.
  • The screen crosses as VT bytes, never as a snapshot. Two agent revisions share no snapshot format. The outgoing agent calls replay() on each live terminal, which is the only place the parser continuation can be read, and the incoming agent feeds those bytes to a fresh terminal of the same size.
  • Check first, and refuse rather than kill. Before it execves, the old agent runs the new binary once to confirm it starts on this host AND can restore sessions (so a downgrade to an app that predates hand-off keeps the newer agent running). If the check fails, or the old agent predates the hand-off request, it keeps running and the app talks to it over the versioned envelope. A failed execve returns to the old program with nothing lost.
  • The residual risk is a new binary that passes the check and then fails while it restores. Its test is to crash the restore on purpose.
  • What it ends. Today an older local agent lives until 30 s after its last terminal closes (DEFAULT_IDLE_TIMEOUT), which can be days, and GitHub status, port forwarding and the busy/idle verdict have no native fallback while it does.

Acceptance criteria

  • On app update, the running local agent hands off to the bundled one: the same shells survive with the same pids, their exit codes still reach clients (the_shells_exit_code_becomes_the_clients), and each reattaching pane is repainted.
  • The listening socket is never unbound during the hand-off.
  • The pre-check refuses a binary that cannot start or cannot restore, and the old agent keeps running. An agent that predates the hand-off request is not replaced.
  • A test crashes the restore on purpose and records exactly what is lost.
  • Hand-off ships on Nightly first. The pre-check, the refuse fallback and the crashed-restore test gate the stable channel.

Blocked by

None. This is local-only and can run in parallel with #227–#229.

Filed by Claude Code at the owner's request, 2026-09-24.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions