Phase 5: remote agent observation — forwarded muxad, per-host badges, Mac-side alarms#4
Merged
Merged
Conversation
…larms (Phase 5) The mirroring half already handled remote hosts, but agent observation was local-only: every muxad join was scoped to the .localAmux engine, so an ssh->tmux->claude workflow showed the terminal without any agent state. - RemoteTmuxHost+MuxaForward: local forward socket path (AF_UNIX budget, slug+connectionHash scheme), remote muxad socket probe (mirrors muxad's XDG/tmp bind rule) + stdout parser, ssh -N -L invocation builder. - AmuxRemoteMuxaForwarder: actor holding one host's muxad socket forward open over the existing SSH ControlMaster; respawned lazily by the status service's reconnect loop via prepareConnection. - AmuxAgentObservationHub: composes the local status service with one forwarder+service pair per SSH host that has a live mirror; reconciles on RemoteTmuxController.mirrorTopologyDidChange (sessionMirrors didSet) and aggregates attendTarget/agentPane across daemons. - RemoteTmuxController: host-scoped joins mirrorWorkspace(hostId:...) that the localAmux joins now delegate to, plus activeSSHMirrorHosts(). - AmuxAgentAlarmPolicy: waiting_input/waiting_choice/error entry and working->idle (finished) transitions become workspace-scoped cmux notifications with per-agent-state cooldown — remote agents now alert on this Mac instead of only on the remote host; local agents gain the same completion alarm. - debug.amux.mirror_ssh: DEBUG-only single-session SSH mirror RPC so E2E against a shared host doesn't mirror every session like remote.tmux.mirror. - Localization: 3 new alarm title keys x 19 locales. - Tests: AmuxMuxaForwardTests (9) + AmuxAgentAlarmPolicyTests (8), wired into cmux.xcodeproj, all passing via cmux-unit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…input notification Verified live in the ssh-localhost E2E: the working->idle alarm was showing the earlier permission-request message instead of what the turn was about. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An ssh -N mux client left behind by a quit pins the host's ControlMaster past its ControlPersist window (an attached session stops the countdown), so applicationWillTerminate now stops the observation hub, and the forwarder's stop() is nonisolated over an OSAllocatedUnfairLock-held process handle (lock carve-out: an actor hop scheduled during termination may never run). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rocess Observed against OpenSSH: a multiplexed `ssh -N -L` registers the forward with the ControlMaster and exits — the master owns the listener. So ensureForward() now treats a connectable local socket as 'already forwarded' (re-requesting would stack listeners on the master), the readiness wait no longer reads process exit as failure, and stop()/the app-termination hook are documented as unlink-the-path teardown (the master's orphaned listener fd dies with the master's ControlPersist window). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 5: remote agent observation (ssh → tmux → agent, observed on the Mac)
The mirroring half of amux already handled remote hosts; the observation half was local-only — every muxad join was scoped to
.localAmux, so the primary target workflow (ssh → tmux → claude) showed the terminal with zero agent state, and completion alarms fired on the remote host instead of the Mac. This PR closes that gap end-to-end.What's new
RemoteTmuxHost+MuxaForward— local forward socket path under~/.cmux/ssh/(same slug+connectionHash scheme and AF_UNIX byte budget as the ControlMaster path), a remote probe that mirrors muxad's own bind rule ($XDG_RUNTIME_DIR/muxa.sock, else/tmp/muxa-<uid>.sock; prefers a live socket, else predicts the default so the forward works the moment the daemon starts), and thessh -N -Linvocation (multiplexed over the existing ControlMaster,ExitOnForwardFailure=yes).AmuxRemoteMuxaForwarder(actor) — holds one host's muxad socket forward open; the status service's reconnect loop callsensureForward()before every connect, so a dead forward heals on the existing retry cadence.AmuxAgentObservationHub— composes the local status service with one forwarder+service pair per SSH host that currently has a live mirror. Reconciled fromRemoteTmuxController.mirrorTopologyDidChange(adidSetonsessionMirrors); aggregatesattendTarget()(longest-blocked across all daemons) andagentPane(inWorkspace:), so attend, the prompt composer, and the choice sheet now work for remote agents too.mirrorWorkspace(hostId:sessionName:)/(hostId:containingPane:); the.localAmuxjoins now delegate to them, and two hosts with same-named sessions can never cross-match.AmuxAgentAlarmPolicy— state transitions become workspace-scoped cmux notifications: enteringwaiting_input/waiting_choice/error, plusworking → idle(finished), with a per-agent+state 20s cooldown. Remote agents now alert on the Mac; local agents gain the same completion alarm. 3 new title keys localized in all 19 locales.debug.amux.mirror_ssh(DEBUG-only RPC) — mirror ONE named session on an SSH host, so E2E against a shared machine doesn't attach to every session likeremote.tmux.mirrordoes.E2E evidence (ssh localhost as the remote host, live muxad v3)
debug.amux.mirror_ssh {"host":"localhost","session":"amux-e2e-remote"}→ topology hook fired, observer created,~/.cmux/ssh/muxa-localhost-….sockbound and answering muxad protocol (v3 hello).user_prompt_submithook in the remote pane → sidebar badge1 workingon the mirror workspace (pane-id join; the installed daemon doesn't emittmux_sessionyet).notification (permission_prompt)→ badge1 waitingandnotification.store.addalarm with the permission message as body.user_prompt_submit→stop→ finished alarm (working → idle).amux.attend→ selected the remote mirror workspace.ssh -Ngone, forward socket unlinked.Unit tests:
AmuxMuxaForwardTests(9) +AmuxAgentAlarmPolicyTests(9), wired into the pbxproj (test-wiring lint passes), all green viacmux-unit.Localization audit
New user-facing strings are the 3 alarm titles — added to
Resources/Localizable.xcstringsfor all 19 locales. Alarm subtitle/body are runtime data (workspace title, agent text). The new debug-only RPC'sinvalid_paramsmessages are bare English, matching the siblingdebug.amux.parse_choicesprecedent.Post-E2E hardening (follow-up commits on this branch)
applicationWillTerminatestops the hub;AmuxRemoteMuxaForwarder.stop()isnonisolatedover anOSAllocatedUnfairLock-held process handle (lock carve-out with justification) so teardown can run synchronously where an actor hop might never be scheduled.ssh -N -Lregisters the forwarding with the ControlMaster and exits — the master owns the listener (verified withlsof: the listener fds sit on the[mux]master).ensureForward()therefore treats a connectable local socket as already-forwarded (re-requesting would stack listeners on the master), and teardown is unlink-the-path (the master's orphaned listener fd dies with the master's ControlPersist window).Known limitations / follow-ups
muxa initthere). Wiring hooks over SSH from amux onboarding is a follow-up.last_responseDTO + prompt/response text in the GUI is the next slice (option b from the review).🤖 Generated with Claude Code