cmux NEXT: one machine link state for the sidebar and the agent pane; a down machine reconnects on use
Item 2 of the machines audit: one feed of machine state, which the sidebar's machine header and the agent pane both read, plus a reconnect when you use a machine whose link is down.
Leo, 2026-10-28: "you can just not choose a project ... you just get a little green dot ... as if you were working on your own computer, but now it's remote."
Leo, 2026-10-28: "whatever's convenient for cloud, like that can be productionized, operationalized, we ought to just put out there"
What changed
-
One word per machine (
MachineLinkState, CmuxNextSidebar). EverySidebarMachine.Statusmaps to one ofconnected,connecting,offline,pausedorattention.connectedmeans the daemon finished its handshake (DaemonHandshake). A bare socket or ssh connect still reads asconnecting. -
The sidebar header draws that word.
- Connected: a green dot.
- Connecting, reconnecting or installing: the small spinner (
StatusIndicatorView.spinner()), never a pulsing dot. - Offline, unreachable, failed or refused: a red dot.
- Paused: grey.
- Install needed: the attention dot.
This Mac still draws no dot while connected. An optional update reads as connected (the green dot, with its words in the tooltip), as in the composer strip.
-
The agent pane gets the same word (
AgentPaneMachineLink, CmuxNextAgentPane). The App's tab store owns one instance and hands it to each pane's model. For a chat on another machine, the page gets amachineLinkhost event ({machine, link, words}), built from the same header builders the sidebar uses (SidebarBridge.paneLink). That makes the composer strip and the sidebar agree by construction. The event goes out when the page loads and again on every change. The strip that draws it is the follow-up PR. -
A down machine reconnects on use. A remote tab's attach client asks the machine link when any part of its connection fails: the endpoint, the socket or the daemon handshake (
AgentSessionAttachClient.Reconnect, fromAgentPaneMachineLink.reconnecting(machine:)). When the machine's link is down, it does three things:- runs the header's own Reconnect (the
machine.reconnectaction: ssh reconnect, server wake, or Cloud retry); - waits for the handshake;
- tries the call once more.
The turn you sent goes out without you pressing Reconnect or Retry. A machine that needs you (sign-in refused, an update required) is never retried; its call fails as before, and a reconnect that ends there fails the send at once instead of waiting out the 45 s.
- runs the header's own Reconnect (the
-
The remote machines' sidebar groups follow acpmux's link pushes. acpmux now pushes
_acpmux/peer_state_changedto its watchers when a peer's link changes, and_acpmux/peerssaysconnectedonly after the handshake (#26756, #26823). The app's own acpmux watch (AgentTurnStateFeed) reads the push and hands it toRemoteMachinesSidebar:- a listed peer's header goes up or down at once;
- a repeat changes nothing (a lagging watcher gets every peer's last note again);
- an unknown peer, an
offlinefor one already down (it may have been removed), or a reopened watch reads_acpmux/peersagain. - a peer list read that a push overtook (its answer may be from before the push) is dropped and read once more.
Only this Mac's own acpmux counts. The Chief home's acpmux, which the app mirrors, never reaches the sidebar, and no peer's states are taken from a peer link. An older daemon sends no push, and the sidebar reads the list on a lane change, as before.
-
From #25993: the
machine.reconnectpane action and its App routing, with its test.
Observers and timers (stop condition and cleanup)
AgentPaneMachineLinkobservation task.- What it is: one
ObservationStreamover the machines' link state for the pages it has pushed to. It is event driven, with no polling. - Stops: it is cancelled and replaced when a page joins or
stateis set. It ends when no page is left (pages are held weakly) and once its owner is gone ([weak self]).
- What it is: one
awaitLinkwait.- What it is: one per machine, shared by every chat on it.
- Stops: on the handshake (an
ObservationStream), on a one-shot deadline (budget, 45 s), or on cancellation. The task group cancels the other child, and the wait is removed fromwaitswhen it settles.
- Header spinner: it turns only while the header's tone is
working, and stops (spin(false)) on any other tone. - Page teardown: a tab's page that is released (
AgentTabStore.release) leaves the link (forget), and the last one gone ends the observation. - Peer state pushes: no new task, timer or observer. The push is handled in the existing watch connection's main-actor batch, so it stops when the watch stops and restarts with it. Each fresh page of the watch (its first, and after every reconnect) re-reads the peer list. The sidebar side does nothing before
bindor afterstop(), and its one list read at a time is bounded by its 2 s deadline and cancelled bystop(). - Cancellation: a cancelled send stops waiting at once, runs no Reconnect, and is not retried.
Captures
Below are before/after stills of the real sidebar list: one ssh machine per state (connected, connecting, reconnecting, offline). They come from the AAMachineLinkGalleryTests still.
No capture: fleet capture is unavailable because the capture controller has no free disk.
Risk
Medium, contained to remote machines.
- The reconnect hook only changes what happens after a remote attach connection has already failed. Its one extra try is gated on the machine's link being down, or on the way and then up within 45 s.
- The header's dot colours keep their meaning; the visible change is a spinner instead of a static dot while connecting.
- No new strings, no new polling, and no settings change.
Tests
On the fleet (package-test-lane.sh, Xcode 26.6): MachineLinkStateTests, AgentPaneMachineLinkTests, AgentPaneConnectActionTests, HostGroupHeaderStatusTests, PeerStatePushTests and AAMachineLinkGalleryTests.
everyStatusHasOneWord: one word per status, and green only for a finished handshake.onlyADroppedLinkReconnectsOnUse: only offline, unreachable and failed reconnect on use.theHeaderDrawsTheWord: the header's tone, with a spinner while connecting.aDownMachineReconnectsOnUse: a failed connection reconnects, then retries once.theWaitEndsAtTheHandshake: the wait ends on the handshake, at once on a link that needs a person, and on the deadline when it never comes.machineReconnectRunsOnAGesture.PeerStatePushTests: the watch decodes the push; a push from a mirrored acpmux never reaches the sidebar while the app's own does; a listed peer follows at once, a repeat changes nothing, and an unknown or removed peer or a reopened watch reads the list again; a push during a read, one that changes nothing shown included, is followed by one more read, and nothing reads afterstop().
The Swift hold for a full base run (#26809) is lifted. The package compile in this PR's own CI is the gate: this PR does not land before it is green.
Not in this PR
- An idle-paused Cloud VM (grey, not red) is not resumed by a send on this path. That is unchanged from base; a follow-up will call the Cloud session's resume before the endpoint.
Supersedes #25993 (its pieces are reused here).
Agent session: agent session
š¤ Generated with Claude Code