acpmux: a DEV build's daemon stops once its app is gone

Closed
AU
6d ago
+453-1

Summary

The app starts its acpmux daemon detached so the daemon outlives the app (its sessions are durable state). For a throwaway tagged DEV build that meant forever: scripts quit DEV apps or delete their bundles without stopping the daemon, which then runs under launchd and idles at about 10% of a core. On one Mac on 2026-10-21, 23 such daemons from dead DEV builds held about two cores (part of a load average near 400). The dead app's headless owners stay connected to the daemon, so no "no client" rule can fire.

A DEV daemon now stops itself through its ordinary shutdown (hosted agents are kept for the next daemon, as on SIGTERM) once its app is gone:

  • Only when the bundle around the daemon's executable is com.cmuxterm.app.debug[.<tag>] (read from its Info.plist at start), on macOS, and the app named its control socket in CMUX_SOCKET_PATH.
  • Every 10 minutes it connects to that socket and closes at once. The app counts as gone when the socket refuses after it answered at least once, or when the daemon's own executable was deleted (the tag bundle was removed). After an hour gone, the daemon shuts down. An app that never listens (socket control off, a socket that failed to start) keeps its daemon. This is a socket connect, not a process scan (check-no-process-scan).
  • The bundle is read before the ready line, so a bundle deleted right after launch still counts.
  • Release, NIGHTLY and Cloud daemons never start the watcher. Same shape as the headless owner's dev_orphan_exit.rs.

Known gaps (review notes, left as is):

  • If the app's socket is not up at the daemon's first try and the app quits before the next one (10 minutes later) while its bundle stays, the daemon still never stops. The app binds its socket before it opens an agent pane, so the first try normally succeeds.
  • A moved or renamed DEV bundle counts as deleted.
  • The orphaned headless Chief server and optchat-chief host of a dead DEV app are not stopped here; they need their own owner signal (follow-up).

Testing

Regression test committed before the fix (tests/dev_orphan_exit.rs): a daemon run from a fake cmux DEV bundle with the debug-only short intervals and a listener standing in for the app's socket.

Command: cmux-tui.yml focused mode, test_filter=dev_orphan (the dispatch scripts/verify-cmux-tui-hosted.sh --filter dev_orphan makes), on hosted Linux and macOS.

  • Test-only head 0e584061a4d2, red on macOS as expected: https://github.com/manaflow-ai/cmux/actions/runs/39579592112. dev_orphan_exit_stops_a_dev_daemon_after_its_app_quits failed ("the DEV daemon kept running 20s after its app quit") and so did dev_orphan_exit_stops_a_dev_daemon_whose_bundle_is_deleted; ..._keeps_..._app_never_listens and ..._never_stops_a_nightly_daemon passed (2 passed, 2 failed).
  • Fix head 8518234c1c04 (fix, a merge of current feat-cmux-next, Linux clippy fix): https://github.com/manaflow-ai/cmux/actions/runs/39629890886. green on macOS: all four dev_orphan_exit_* integration tests and all six daemon::dev_orphan::tests::* unit tests passed; test (linux) passed. That job's own red is only the base clippy error in crates/cmux-tui/tests/vm_child_sessions_journey.rs:170 (std::slice::from_ref), which this PR does not touch. The first macOS attempt was refused by its runner for disk space and was rerun on the same SHA.
  • On both, test (linux) passed (the Linux tests: NIGHTLY and off-macOS keep serving, plus the unit tests). Both macOS jobs were still waiting for a hosted runner after an hour.

Review: a correctness review subagent returned FIX (an app that never listens would lose its daemon), fixed in this head, then PASS on the re-review.

Observed / Not run

  • Observed on 2026-10-21: 23 orphaned DEV daemons, each with its app gone, at about 10% CPU each. Each app daemon's environment names its app's control socket in CMUX_SOCKET_PATH, and that socket is gone once the app quits.
  • python3 scripts/cmux-next/prepush-gate.py: no new violations.
  • lint (linux) failed before the merge on clippy unnecessary_sort_by in cmux-worktree-care/src/store.rs:97, a file this PR does not touch. The base tip has already fixed it, and this head merges that fix. A rustfmt import order in the new test was also fixed. The first hosted run (https://github.com/manaflow-ai/cmux/actions/runs/39563749392) got no macOS runner in 2 h.
  • The fleet's cmux-ci run refused scripts/ci/cmux-tui-focused-test.sh and scripts/ci/cargo-test-selected.sh (not on the ci-step allowlist), so the macOS tests wait for the hosted runner.
  • Not run: no local cargo (repo rule); no tagged DEV app dogfood. The behavior is DEV-only and covered by the integration test with a stand-in socket.

Changelog

none

šŸ¤– Generated with Claude Code