Skip to content

cmux NEXT: the app takes over a stray acpmux on its home instead of refusing it - #23598

Merged
teamleaderleo merged 5 commits into
feat-cmux-nextfrom
cmux/acpmux-absorb-stray
Oct 21, 2026
Merged

teamleaderleo merged 5 commits into
feat-cmux-nextfrom
cmux/acpmux-absorb-stray

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

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 ~/.acpmux with 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 (findOrStart with startsDaemon), 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 reports agentHosts; 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 the services list of launchctl print gui/<uid>, then confirms the job with launchctl print gui/<uid>/<label> (pid = N). If it finds one, the app runs launchctl bootout on it, so KeepAlive does not start it again. launchd's SIGTERM goes through the same shutdown_all as _acpmux/shutdown, so the agents detach (agent hosts setsid). The job is not disabled: acpmux host setup/update bootstrap that same com.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 by host 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/shutdown over 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):

  • chatsReachTheBundledAcpmuxWhenAStrayDaemonHoldsTheHome starts the same-tree acpmux from a local/bin copy on a home, then the app's host from its own bin copy. 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.
  • onlyADaemonOutsideEveryAppBundleIsStray covers what counts as stray.
  • theLaunchdJobThatRunsTheStrayDaemonIsFoundByItsPid covers 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/CmuxNext on a macOS mini (PR CI skips these suites because they need the same-tree acpmux):
    • Red, on base. chatsReachTheBundledAcpmuxWhenAStrayDaemonHoldsTheHome alone on base 7eed529 (scratch branch cmux/acpmux-absorb-stray-red @ 34cbc8b). 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, because AcpmuxStrayDaemon does not exist yet, so the base run is the behavioural red.
    • Green, at head 8d2a276. All three AcpmuxStrayDaemonTests pass, AcpmuxBundleReplacedTests passes, and no suite was skipped for needing the same-tree acpmux.
  • CI at 8d2a276: fleet/compile-check, Release compile, fast tier and generated files are green. cmux-next swift test fails, 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 in AgentPaneHost.findOrStart. Base push runs skip swift test, and other open PRs fail different UI suites in the same job. Tour the app failed while setting up its runner.
  • Known residuals (low, from the 8d2a276 review): (1) a launchctl bootout that 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

teamleaderleo and others added 2 commits October 21, 2026 17:33
… move to the bundled daemon); a launch agent that runs it is booted out

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ca9-5ac5-7692-adfc-a3ae828256ca&host=huge-blue
@teamleaderleo teamleaderleo added the risk:high Risk tier (docs/ci/risk-tiers.md) label Oct 21, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by acpmux-unify on huge-blue · session

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 046a080: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr23598 -f ref=cmux/acpmux-absorb-stray -f platform=macos -f backend_mode=local

Not verified: macOS jobs did not run: compile and app tests were skipped on 046a0802e0; ci-status is green without them.

CI passes on 046a0802e0 (run 39680901479 attempt 1).
macOS jobs did not run: compile and app tests were skipped: Fast static checks skipped (the macos job needs both to succeed). Re-run CI once they pass, before merging.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

Tour of 8d2a276306 (run): no media. The tour job failed before the tour ran (build or setup); the run log has the output.

…id, boots it out without disabling it, and trusts only the socket's own peer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ca9-5ac5-7692-adfc-a3ae828256ca&host=huge-blue
@cursor

cursor Bot commented Oct 21, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Bead cx-lvxp7 created for this PR by the cmux coordinator.

…s is the daemon, never an app; a failed bootout falls back to the shutdown request

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15ca9-5ac5-7692-adfc-a3ae828256ca&host=huge-blue
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (independent Claude subagent, acpmux-unify lane): SHIP at 8d2a276

This is the third review. The first two failed (d6a14bb, 1ce6a13); every finding from them is fixed:

  • no persistent launchctl disable;
  • the launchd job is found by the daemon's own pid, confirmed per label, never a parent and never an application.* job;
  • only the socket's own peer is acted on (LOCAL_PEERPID == the reported pid, own uid, a stray path);
  • a failed bootout falls back to _acpmux/shutdown.

Scope: another install's daemon (.app/Contents/), the app's own, the Chief's (startsDaemons: false), reconnects and other homes (serves(home:) first) are never taken over. A daemon without agent hosts is refused. Agent hosts setsid, and SIGTERM takes the same shutdown_all path, so no agent ends.

Low residuals are listed in the body. Swift 6 compile checked by reading.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (independent Claude review subagent): SHIP at 046a080

One base merge of feat-cmux-next ea0e0ed. Reviewed the full PR diff from that base plus the merge's effect on the APIs the PR uses (all present with the same signatures). No defects. Two non-blocking notes: a failed takeover is not retried in the same process (deliberate, no loop), and a bootout that stops the daemon but returns non-zero shows one refusal before the next attempt starts the bundled daemon.

@teamleaderleo
teamleaderleo merged commit d9e170c into feat-cmux-next Oct 21, 2026
@teamleaderleo
teamleaderleo deleted the cmux/acpmux-absorb-stray branch October 21, 2026 21:44
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 046a0802 Deployed Oct 21, 2026 by teamleaderleo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant