cmux NEXT: a refused acpmux names its binary, and Restart acpmux replaces it with this app's

Merged
TE
11d ago
+505-27

Leo: "also lawrence ran into this for some reason"

claude-sr2, word for word: "#24420, then #24421: a refused acpmux offers Restart. Lawrence's 34a5ea4 on base already names the binary differently, so rebase onto it and keep only the Restart part. Don't rework his code. If the two conflict in substance, tell me and I'll take it to Leo."

Adopted from a lane that died on cmux16, rebased onto current feat-cmux-next, and cut down to the Restart part. The web half (the card's button) is #24421.

Rebase onto base's refusal copy

The earlier version also named the binary and its signing team in the refusal message. Base already names it its own way: Lawrence's 34a5ea4, then #24795 (c7daff21835), which is what base shows now (AgentPaneStrings.swift: "acpmux belongs to %@, and this build may not use it. Quit that app, then try again." / "Another acpmux service is using this socket. It will reconnect when that service is ready."). This PR now changes none of that text, keys or the retry behaviour: AgentPaneStrings.swift only gains the new associated value in its two patterns, and Localizable.xcstrings is untouched. The binary-naming parts (UnixSocketPeer.signingTeam, daemonOwnerSigned/daemonOwnerUnsigned, the owner sentence, the team in the log line) are dropped. Restart acpmux does not need the binary named in the message; it does not conflict with base's copy.

What changes

  • A refused handshake whose socket another binary of this user holds, on a host that starts daemons, carries that binary as owner (AcpmuxSocketOwner: its path and the socket it was read on) and is marked details.restartAcpmux.
  • A new page op acpmux.restart (needs the person's click and a refusal that offered it) stops that daemon the way an update hands one off: a launchd job that runs it is booted out first, else _acpmux/shutdown, on ONE connection whose peer was read on that descriptor. Its agents detach and its sessions stay; the page's next handshake starts this app's own acpmux.
  • It never stops this app's own acpmux, another user's daemon, a daemon without agent hosts, a daemon of another binary than the refusal named, a daemon on another socket than the refusal's, an attached environment's, or the Chief home's: a refusal routed to the Chief home's acpmux offers no restart (AcpmuxChiefSessionRouter). The launchd bootout runs only while the peer's connection is still open, so a reused pid is never booted out.
  • The peer check itself is unchanged: nothing new is trusted.

Risk: high, because it adds a page op (AgentPageOps).

Tests

cmux-ci run --class light --script scripts/ci/package-test-lane.sh --arg suite --arg Packages/macOS/CmuxNext --arg AcpmuxRefusedRestartTests,AcpmuxRefusedRestartDaemonTests,AcpmuxRefusedDaemonTests,AcpmuxStrayDaemonTests:

  • red at 37177b7e1a (branch hb-orphan-restart/red-24420: the new tests, without the socket and Chief guards): restartAcpmuxNeverStopsADaemonOnAnotherSocket and aChiefRefusalOffersNoRestart fail (the other-socket restart reaches the daemon; the Chief refusal still carries its owner), the other restart tests pass (fleet step 6daebbee50c29ccd763aa07f).
  • green at c2813a12ca and again at the head c25e98bad2 (fleet step for key hb-orphan-restart-24420-c25e98bad): AcpmuxRefusedRestartTests 7/7 and AcpmuxRefusedRestartDaemonTests 1/1 pass (the end-to-end restart ran, not skipped), AcpmuxStrayDaemonTests 3/3 pass (fleet step 0e0b8112d2a8dfc83e4b6cc4). AcpmuxRefusedDaemonTests has 2 failing tests (theMessageNamesTheBuildAndTheSocket, everyRefusalNamesTheBuildWithoutACommand) that check message text base changed in #24795; they fail against base's own code (read, not run: base's test at AcpmuxRefusedDaemonTests.swift:23,38,39 still expects the socket path and "Quit ", which #24795 removed from base's copy at AgentPaneStrings.swift:191-194), and this PR changes neither that text nor those tests.

Tests added for review findings: restartAcpmuxNeverStopsADaemonOnAnotherSocket, aChiefRefusalOffersNoRestart, restartAcpmuxStopsTheNamedDaemonOnItsOwnSocket (a fake daemon records initialize, status, shutdown on one connection). The end-to-end AcpmuxRefusedRestartDaemonTests no longer expects a refusal that base accepts (another install's acpmux with the person challenge): it sets the offer as a refusal does and checks Restart replaces the daemon with this app's own.

Review

Codex gpt-6-astra xhigh, independent read-only sessions. Round 1 BLOCK: a Chief refusal could restart a daemon on this Mac's socket; the real-daemon test expected a refusal base accepts. Round 2: Swift findings fixed. Round 3: SHIP at 451b1cc1db. Then a test-only compile fix (a key path inside #expect): SHIP at c2813a12ca. Then the crash ratchet flagged async_closure_default_arg (my test hooks were async closure defaults on takeOver); they moved to an overload, and the ratchet passes. Fresh Codex xhigh review: SHIP at c25e98bad22fb446b08ec5f125fc6830fe6eca0b.

No capture: this PR changes no visible text or view (base's refusal copy stays); the card and its Restart acpmux button are #24421's, which has before/after captures.

Stop conditions: no new timer or observer. Restart waits at most 5 s per request and 15 s for the old daemon to exit.

Changelog

  • Fixed: a chat refused because another acpmux holds the socket can restart acpmux from cmux.

Agent session: big-red
šŸ¤– Generated with Claude Code