Skip to content

sidebar: remote agents over ssh (state mirror + jump routing) - #233

Open
sophronesis wants to merge 1 commit into
raine:mainfrom
sophronesis:remote-agents-pr
Open

sidebar: remote agents over ssh (state mirror + jump routing)#233
sophronesis wants to merge 1 commit into
raine:mainfrom
sophronesis:remote-agents-pr

Conversation

@sophronesis

@sophronesis sophronesis commented Aug 12, 2026

Copy link
Copy Markdown

Note

AI disclosure, upfront: this feature was implemented with Claude Code (model claude-fable-5; the 2026-08-14 revision below used claude-opus-5) - designed, driven, reviewed and live-tested by me, but the code was largely machine-written. The commit carries Co-Authored-By trailers for both. If AI-assisted contributions aren't welcome here, feel free to close - no hard feelings.

I know CONTRIBUTING prefers an issue first for large changes - happy to convert this into an issue/discussion instead. Opening as a PR because the implementation exists and has been running on my machines all day (laptop + a work server, 5 agents of which 2 remote), so it felt more concrete to discuss with code attached.

Important

Updated 2026-08-14 (force-push, the single commit amended - no review had happened yet, so I kept the history clean rather than stacking a fixup).

Running this for real for two days turned up the one way the feature goes bad. Remote state files outlive their panes - nothing prunes them unless a workmux daemon happens to be running on that machine - so a sync that copies the agents dir wholesale pins dead agents to the sidebar forever. I had two ghosts from a rebooted remote tmux server sitting in my sidebar for a day before I noticed.

No behaviour change in the Rust code (the reconcile comment is the only source edit): the fix belongs in the sync contract, which is the part this PR documents rather than implements. So:

  • the guide now requires the sync to drop an agent whose pane is gone, with the four checks spelled out (pane_id present in list-panes -a, boot_id vs #{start_time}, pane_pid, command vs #{pane_current_command}) - the same ones the local reconcile pass applies to local panes
  • the example script does that now, and was rewritten anyway: the old one line-parsed grep -H . *.json, which never worked, because upsert_agent writes with to_string_pretty and the files are multi-line
  • added a tip that if your only window into the machine is an ssh pane in local tmux, it is worth mirroring only while that pane exists - an agent you cannot jump to is not worth a tile

The reconcile comment now says outright that these files are a mirror, not the state of record, so a future reader knows why no liveness check happens there.

What this adds

I keep claudes running inside tmux on other machines (attached via autossh ... 'tmux attach' panes) and wanted them in the same sidebar as local agents. The kitty/wezterm/zellij backends and pure-local setups are untouched.

  • remote agents in the sidebar/dashboard: state files mirrored from another machine (with pane_key.instance rewritten to ssh:<host>) are reconciled without local liveness checks and exposed under namespaced pane ids ssh:<host>/%N
  • jump routing over ssh: switch_to_pane recognizes the namespace; jumping switches the agent's window in every attached session of the remote server (grouped sessions keep per-clone active-window pointers, so a bare select-window isn't enough), focuses the most recently used local ssh/autossh attach pane for that host, and runs the remote flip as a detached ssh so the UI never blocks (local switch lands in ~30ms)
  • active-row highlight follows remote jumps via a @workmux_remote_active tmux option - recorded on jump, validated by the daemon, cleared as soon as the user moves to a different local window (remote agents have no local window for the normal host-window match)
  • {remote} template token: dimmed @<host> tag next to the agent name, wired into the default compact/tile/horizontal templates
  • remote agents are skipped by pane capture, git stats, and PR checks (those need local panes/paths)
  • docs: new "Remote agents" guide page (concept, sync contract with a minimal example script, jump/highlight semantics, caveats), {remote} in the token table, nav entry

Design notes

  • workmux itself opens no network connections except during an explicit jump. The state mirror stays outside workmux on purpose - any ~1s sync loop satisfying the documented contract works (copy agents/*.json, keep only agents whose pane is still live on the remote, rewrite instance, delete mirror when unreachable, optionally SIGUSR1 the daemon). This keeps auth/transport policy out of the tool - at the cost of making liveness the syncer's responsibility, which the guide is now explicit about.
  • tmux backend only; other backends' switch_to_pane are untouched, and the sync contract only mirrors backend == "tmux" states.

Testing

  • cargo fmt --check clean; clippy warnings in the diff: none (remaining ones pre-exist on main)
  • unit tests: 1416 passed; sandbox::rpc::tests::test_exec_sandbox_blocks_ssh_read fails on my box also on a clean checkout of main (environment-dependent), passes in a sandboxed build
  • no new integration tests: exercising this properly needs two machines; happy to add a mocked variant if you want this landed
  • soak-tested for a day: status latency remote→sidebar ~1s end to end, jump verified against grouped sessions with multiple attached clients
  • 2026-08-14 revision: docs + comments only, no Rust behaviour change (so the numbers above still stand). The rewritten example script was run against a real remote - mirrors the live agents, prunes a seeded dead tile, and correctly drops two state files left behind by a restarted remote tmux server.

🤖 Generated with Claude Code

Show agents running on other machines in the local sidebar/dashboard
and route jumps to them over ssh:

- reconcile: state files with instance "ssh:<host>" (mirrored from a
  remote machine by an external sync) are exposed with namespaced pane
  ids "ssh:<host>/%N", skipping local liveness checks
- switch_to_pane routes ssh:* pane ids through remote_pane_jump:
  local-first for perceived latency - focus the most recently used
  local ssh/autossh attach pane for that host, record the highlight in
  @workmux_remote_active, then flip the remote group's window pointers
  via a detached ssh (every attached session; grouped sessions keep
  per-clone active-window pointers, so a bare select-window is not
  enough)
- the sidebar daemon validates/clears the remote-active override
  against the recorded window and ships it in snapshots; clients
  prefer it when picking the active row
- new {remote} template token renders a dim "@<host>" tag next to the
  agent name, wired into all default templates
- remote agents are skipped by pane capture (no local pane to read)

The state mirror itself lives outside workmux: any 1s-ish sync that
copies ~/.local/state/workmux/agents/*.json from the remote host and
rewrites pane_key.instance to "ssh:<host>" makes the remote agents
appear.

Because the local reconcile pass has nothing to verify them against,
that sync owns liveness. Remote state files outlive their panes -
nothing prunes them unless a workmux daemon happens to run over there -
so a sync that copies the directory wholesale pins dead agents to the
sidebar forever. The contract is to drop an agent whose pane is gone
(id absent from list-panes, or boot_id / pane_pid / command disagreeing
with the live pane) and to clear the mirror when the host is
unreachable; the guide page spells it out with a working example.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@raine

raine commented Aug 30, 2026

Copy link
Copy Markdown
Owner

The idea is fun but seems a bit hacky

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants