acpmux: a DEV build's daemon stops once its app is gone, also when its binary is rebuilt (carries #23279)

Merged
TE
6d ago
+662-1

Carries #23279 (austinywang's five commits, authorship kept), merged onto current feat-cmux-next, plus the fix for the 2026-10-26 review's BLOCK. #23279 has sat conflicting since 2026-10-21, so this PR takes it over.

For Leo's ask: "JUST FING REMOVE THE FING OLD DAMEMONS".

What #23279 does

A DEV build's acpmux daemon (its bundle is com.cmuxterm.app.debug[.<tag>], on macOS) checks the app's control socket (CMUX_SOCKET_PATH) every 10 minutes. If the app has been gone for an hour, the daemon stops through its ordinary shutdown. Tagged DEV apps get quit or deleted by scripts and left their daemons running under launchd forever (23 on one Mac, 2026-10-21). Release, NIGHTLY and Cloud daemons never start the watcher. See #23279 for the full design.

Fixes on top (9200b25)

  • A deleted or rebuilt binary counts as gone, whatever the socket says. On #23279, gone() needed the socket to refuse even when the executable was deleted. A same-tag DEV rebuilt elsewhere listens on the same socket (it is keyed by bundle id + tag), so the old daemon kept resetting its absence clock and never stopped (the BLOCK). The check was also by path only, so a rebuild at the same path went unseen. The watcher now opens the executable at start and holds it open. Once that file has no name left (it was deleted, or replaced by a new file at its path), the app counts as gone. A write in place is deliberately not a new build. No build writes a binary in place, because overwriting a signed Mach-O gets the process killed: scripts/install-cmux-tui-client.sh and scripts/cmux-next/bundle-cmux-tui.sh delete the old file before copying the new one. A size or mtime check would also stop a running app's daemon after a touch or a re-sign, and end its agents. An earlier review on this PR blocked exactly that.
  • The orphan exit ends its hosted agents. #23279 used the SIGTERM-style handoff, which keeps hosted agents running for a next daemon. With the DEV app gone, nothing would adopt or end them, so they stayed behind as old processes. The orphan arm now calls end_agents_at_shutdown first, the same call the app's Quit Everything uses.
  • Merge: base already depends on plist = "1.10", so the PR's second plist line (which cargo would reject) is dropped. Other conflicts were trivial; both sides kept.

Not changed

  • An app that never listened (socket control off), with an unchanged executable, still keeps its daemon, as on #23279.
  • The delay stays 1 hour, checked every 10 minutes.

Relation to #27796 (carries #27594)

They complement each other. #27594 stops a daemon whose home is removed. This PR stops a DEV daemon whose app is gone while its home stays (~/.acpmux/builds/<key> survives deleting a bundle). Whichever lands second resolves a small daemon.rs conflict (both select! arms are kept).

Tests

  • Unit (daemon::dev_orphan::tests): a deleted or replaced executable counts as gone while the socket answers; a move, a touch or a write in place does not; a file that could not be opened at start never counts as gone.
  • Integration (tests/dev_orphan_exit.rs, macOS): new dev_orphan_exit_stops_a_dev_daemon_whose_binary_is_replaced, where the app keeps listening and the binary is replaced by a copy. The daemon must stop. #23279's four tests stay.
  • Linux: clippy --all-targets and the lib/unit tests through fast checks (results below once they run). The macOS integration tests need a fleet Mac.

Agent session: agent session

šŸ¤– Generated with Claude Code