cmux NEXT: the app takes over a stray acpmux on its home instead of refusing it
In progress. The review of d6a14bb FAILED; its findings were fixed in 1ce6a13. The re-review of 1ce6a13 FAILED on the parent-pid match (it could boot out a terminal app that started the daemon); fixed in 8d2a276. The re-review of 8d2a276: PASS (low notes below). risk:high (daemon lifecycle): held for coord-deputy before merge.
One acpmux per machine (hq plans/cmux-next/acpmux-one-daemon.md, step 2). When the daemon on the app's acpmux home is not one any cmux install runs, the app now takes it over instead of refusing it. Examples are a ~/.local/bin/acpmux started by launchd (com.acpmux.daemon) or by the CLI's autostart. Before this change, the server peer check refused that daemon because its executable is not the app's, and every chat said acpmux did not start.
- What counts as stray (
AcpmuxStrayDaemon.isStray): the daemon's executable (proc_pidpath, symlinks resolved) is neither the app's acpmux nor inside any.app/Contents/. Another cmux install's daemon is not stray: NIGHTLY shares~/.acpmuxwith older builds, and the peer check's existing rules still decide those (lead decision 2026-10-10). - The takeover (
handOffIfStray) runs only where the app may start a daemon (findOrStartwithstartsDaemon), before the version handoff. It is the same move as an update handoff: the old daemon stops, its agents detach, and the bundled daemon starts and adopts them. It acts only when the daemon reportsagentHosts; an older one would end its agents, so it keeps running and stays refused, as before. It runs at most once per pid per app process. - launchd: the app finds the launchd job whose own process is the daemon, by the daemon's pid. Never its parent's: that may be a terminal or another job that only started it. Never an
application.*job either. It reads theserviceslist oflaunchctl print gui/<uid>, then confirms the job withlaunchctl print gui/<uid>/<label>(pid = N). If it finds one, the app runslaunchctl bootouton it, so KeepAlive does not start it again. launchd's SIGTERM goes through the sameshutdown_allas_acpmux/shutdown, so the agents detach (agent hostssetsid). The job is not disabled:acpmux host setup/updatebootstrap that samecom.acpmux.daemon, and launchd refuses a disabled one. A later login may start it again. If the bundled daemon already holds the home, that stray copy fails on the lock and KeepAlive retries it about every 10 s (the throttle) until it is fixed by hand or byhost update. If the stray wins, the app takes it over again. A bootout that fails falls back to the shutdown request. If no launchd job runs it, the app sends_acpmux/shutdownover the socket. - Who is asked to stop: only the socket's own peer (
LOCAL_PEERPID,AcpmuxServerPeer.socketPeer), and only when its pid equals the pid the daemon reports. That pid's executable must be stray and the peer must run as this user. Before this, the code acted on the pid the daemon reported, without checking it.
Risk: high. This stops a daemon the app did not start, and it boots out the launchd job that runs it (until the next login). It happens only for a daemon on the app's own home that runs outside every app bundle and has agent hosts, and no running agent is ended.
No capture: nothing on screen changes, except that chats connect where they used to say acpmux did not start.
Tests (AcpmuxStrayDaemonTests), committed first in ed07687 (red):
chatsReachTheBundledAcpmuxWhenAStrayDaemonHoldsTheHomestarts the same-tree acpmux from alocal/bincopy on a home, then the app's host from its ownbincopy. Without the fix, the app's start is refused (daemonRefused, other executable). With it, the daemon then runs from the app's bin with a new pid.onlyADaemonOutsideEveryAppBundleIsStraycovers what counts as stray.theLaunchdJobThatRunsTheStrayDaemonIsFoundByItsPidcovers finding the job by pid in the services list and reading a job's pid.
Observed / Not run
- Ran:
scripts/cmux-next/prepush-gate.py(no new violations). - Fleet runs, all
cmux-ci run --class light --script scripts/ci/run-swift-testing-suites.sh --arg Packages/macOS/CmuxNexton a macOS mini (PR CI skips these suites because they need the same-tree acpmux):- Red, on base.
chatsReachTheBundledAcpmuxWhenAStrayDaemonHoldsTheHomealone on base 7eed5290 (scratch branchcmux/acpmux-absorb-stray-red@ 34cbc8be). It fails:the app's launch failed: daemonRefused(holder: …/local/bin/acpmux, … reason: "the socket's peer is …/local/bin/acpmux, not the acpmux this app runs"). The red commit ed07687 itself does not compile the test target, becauseAcpmuxStrayDaemondoes not exist yet, so the base run is the behavioural red. - Green, at head 8d2a276. All three
AcpmuxStrayDaemonTestspass,AcpmuxBundleReplacedTestspasses, and no suite was skipped for needing the same-tree acpmux.
- Red, on base.
- CI at 8d2a276:
fleet/compile-check, Release compile, fast tier and generated files are green.cmux-next swift testfails, but none of its failing tests are on this change's path. They are sidebar render counts, the morphing card, page hibernation, Chief row titles and settings words, while this diff only changes the daemon start inAgentPaneHost.findOrStart. Base push runs skip swift test, and other open PRs fail different UI suites in the same job.Tour the appfailed while setting up its runner. - Known residuals (low, from the 8d2a276 review): (1) a
launchctl bootoutthat exits non-zero while it is still stopping the job, and a socket that is already gone, can fail one chat open; the next open finds nothing on the socket and starts the bundled daemon. (2) A launchd job that starts the stray as a child (not matched, by design) with KeepAlive can restart it. Each later open then takes it over again: agents are kept, but CLI clients on the stray are disconnected each time. - Not run: no Swift build or test ran on huge-blue or on Leo's Macs; all of them ran on the fleet and CI as listed above. The launchd bootout is not exercised end to end; only its parsers are unit-tested. CI must not change a runner's launch agents.
Changelog
Changed: cmux NEXT takes over an acpmux daemon it did not start (such as ~/.local/bin/acpmux) instead of refusing it, so chats connect and the agents keep running.
Leo-ask: ask-ddceee64a4
Agent session: huge-blue
🤖 Generated with Claude Code