Skip to content

Conversation

@ThomasK33
Copy link
Member

Summary

Adds a “Remote Mux server” workflow so the desktop app can show local + remote workspaces in one UI, while running agent loops on an always-on mux server.

Key capabilities:

  • Configure remote servers (baseUrl + token + local↔remote project path mappings)
  • Surface remote workspaces alongside local ones
  • Proxy core workspace APIs so streaming + Ask UX works end-to-end for remote agent loops
  • Create remote workspaces from the desktop creation flow

Background

Users want long-running agent work to continue when a laptop sleeps/closes, without syncing local uncommitted state. A remote mux backend is the simplest way to keep agent loops alive while keeping a single desktop UI.

Implementation

  • Persist remote server registry in ~/.mux/config.json and store auth tokens in ~/.mux/secrets.json under a reserved __remoteMuxServer:<id> key.
  • Added remoteServers.* oRPC endpoints (list/upsert/remove/clearAuthToken/ping + workspaceCreate).
  • Added a filesystem-safe, reversible remote workspaceId codec and remote node-side oRPC client helper.
  • Backend router:
    • Detects encoded remote workspaceIds and proxies core per-workspace endpoints (onChat, sendMessage, answerAskUserQuestion, resumeStream, interruptStream, getInfo, getFullReplay, getSubagentTranscript).
    • Rewrites IDs inside streamed events (including task-created + session-usage-delta and best-effort task tool payload rewriting).
    • Merges remote workspaces into workspace.list, workspace.onMetadata, and workspace.activity.* with retry/backoff on remote subscription failures.
  • UI:
    • Settings → Remote Servers section for editing remote servers and mappings.
    • Creation header “Create on: Local/Remote” + remote server picker; remote creation uses remoteServers.workspaceCreate.

Validation

  • make static-check

Risks

  • Global workspace views now include remote fetch/stream merge logic; failures are isolated per-remote and should not block local UX.
  • Stream/event ID rewriting is best-effort; new stream event shapes may need follow-up rewrites if additional id fields are introduced.

📋 Implementation Plan

Plan: Remote workspaces via “Remote Mux server” (desktop backend proxy)

Context / Why

You want the desktop app to show both local and remote workspaces in one UI, while allowing some workspaces (and their agent loops) to run on an always-on remote mux server so they continue working after you close your laptop.

Key constraint decisions (from the request):

  • Single UI showing local + remote simultaneously
  • Clean git state (no syncing local uncommitted changes)
  • Single-user (shared bearer token auth is acceptable)

Important architectural clarification (current codebase):

  • A Mux “runtime” (local|worktree|ssh|docker|devcontainer) controls where tools execute.
  • The agent loop runs wherever the mux backend runs.

So the feature is best implemented as remote backends + local proxy/aggregator, not as a new Runtime variant.

Goals (MVP)

  1. Configure one or more remote mux servers (base URL + auth token) from the desktop app.
  2. Show remote workspaces in the desktop sidebar alongside local workspaces.
  3. Create a remote workspace for a local project (via explicit local↔remote project path mapping).
  4. Proxy the essential workspace operations so remote agent loops work end-to-end:
    • workspace.sendMessage, streaming via workspace.onChat
    • workspace.answerAskUserQuestion (otherwise remote agents can’t proceed)
    • workspace.interruptStream / resumeStream
    • global subscriptions: workspace.onMetadata, workspace.activity.subscribe
  5. Ensure IDs inside stream events are rewritten so the UI can navigate between parent/child (subagent) workspaces.
  6. Automatically reconnect / resubscribe to remote streaming endpoints when connections are aborted (so the UI recovers without restarting Mux).

Non-goals (initially)

  • Syncing local uncommitted changes to remote.
  • Multi-user / RBAC.
  • Full parity for every workspace-side feature (terminals, editor deep-links, background bash controls, MCP config editing, etc.).
    • We’ll audit which endpoints the UI calls unconditionally and proxy those as needed.

Evidence (repo facts consulted)

  • Remote server already exists: mux server (src/cli/server.ts) + oRPC over HTTP/WS (src/node/orpc/server.ts).
  • Desktop renderer uses oRPC MessagePort locally and supports WS transport generally (src/browser/contexts/API.tsx).
  • Workspace API surface + subscriptions are defined in src/common/orpc/schemas/api.ts (notably workspace.onChat, workspace.onMetadata, workspace.activity.subscribe).
  • Streaming events contain workspaceId and sometimes taskId/sourceWorkspaceId that must be rewritten (src/common/orpc/schemas/stream.ts).
  • Config persistence is ~/.mux/config.json via Config.loadConfigOrDefault() / saveConfig() (src/node/config.ts) and ProjectsConfig (src/common/types/project.ts).
  • Workspace metadata is keyed by workspaceId and session dirs are ~/.mux/sessions/<workspaceId> (Config.getSessionDir() in src/node/config.ts).
  • Existing HTTP proxy patterns exist in CLI (src/cli/proxifyOrpc.ts) and streaming works over HTTP in tests (src/cli/server.test.ts).
  • ORPC context wiring and service initialization: src/node/orpc/context.ts, src/node/services/serviceContainer.ts.
  • Explore reports (trusted):
    • WorkspaceService/session mapping keyed by workspaceId and filesystem-safe ID constraints.
    • UI creation controls live in src/browser/components/ChatInput/CreationControls.tsx + useCreationWorkspace.ts.

Approaches (with net LoC estimate)

Approach A (recommended): Local backend proxy + ID rewriting (single desktop UI)

Net LoC (product code): ~1,800–2,800 (plus tests)

  • Desktop renderer continues talking only to local Electron main (MessagePort).
  • Local backend maintains a registry of remote servers and proxies workspace operations + subscriptions.
  • Remote workspaces are represented locally by namespaced workspace IDs (filesystem-safe) so they don’t collide.

Implementation plan (Approach A)

Phase 1 — Remote server registry + config persistence

Backend + schema

  1. Add a persisted remote server registry to ProjectsConfig.

    • Files:

      • src/common/types/project.ts (extend ProjectsConfig)
      • src/node/config.ts (loadConfigOrDefault + saveConfig parse/write)
    • Suggested shape:

      export interface RemoteMuxServerConfig {
        id: string;            // stable, filesystem-safe
        label: string;         // display name
        baseUrl: string;       // e.g. https://mux.example.com
        projectMappings: Array<{
          localProjectPath: string;
          remoteProjectPath: string;
        }>;
        enabled?: boolean;
      }
    • Store per-server auth tokens in ~/.mux/secrets.json under a reserved key prefix keyed by server id (so config.json can be committed/shared). Example: "__remoteMuxServer:server-id": [{ "key": "authToken", "value": "..." }].

  2. Add an oRPC API namespace for managing servers.

    Security note (auth token storage)

    Store remote server auth tokens in ~/.mux/secrets.json (plaintext by default) so config.json remains shareable/committable (and secrets.json can be encrypted or omitted from dotfiles). For single-user MVP this is likely acceptable, but if the remote server is reachable beyond localhost/LAN we should add warnings + recommend TLS + token rotation. Longer-term, consider OS keychain integration.

    • Files:
      • src/common/orpc/schemas/api.ts (add remoteServers.* entries)
      • src/node/orpc/router.ts (handlers)
    • Minimal procedures:
      • remoteServers.list(): Array<RemoteMuxServerConfig & { hasAuthToken: boolean }>
      • remoteServers.upsert({ config, authToken? }): Result<void, string> (stores token in ~/.mux/secrets.json under a reserved key)
      • remoteServers.clearAuthToken({ id }): Result<void, string>
      • remoteServers.remove({ id }): Result<void, string> (also removes token)
      • remoteServers.ping({ id }): Result<{ version: string }, string> (calls remote general.ping + /version optionally)
      • remoteServers.getProjectContext({ serverId, localProjectPath }): Result<{ remoteProjectPath: string; branches: string[]; recommendedTrunk: string | null; runtimeAvailability: RuntimeAvailability }, string>
      • remoteServers.workspace.create({ serverId, localProjectPath, branchName, trunkBranch?, title?, runtimeConfig? }): Result<{ metadata: FrontendWorkspaceMetadata }, string>
  3. Implement RemoteServersService in Node.

    • New file: src/node/services/remoteServersService.ts
    • Responsibilities:
      • load/save registry via Config.editConfig
      • load/save auth tokens in ~/.mux/secrets.json under a reserved key (never in config.json; only expose hasAuthToken)
      • validate baseUrl, normalize trailing slashes
      • best-effort reachability checks
  4. Wire the service into the oRPC context so router handlers can access it.

    • Files:
      • src/node/orpc/context.ts (extend ORPCContext)
      • src/node/services/serviceContainer.ts (instantiate + expose)
      • src/desktop/main.ts and src/cli/server.ts (include in orpcContext object)

Phase 2 — Remote client + ID/Path rewriting utilities

  1. Implement a remote oRPC client factory.

    • New file: src/node/remote/remoteOrpcClient.ts

    • For MVP, prefer HTTP transport (simpler) using @orpc/client/fetch:

      import { RPCLink as HTTPRPCLink } from "@orpc/client/fetch";
      import { createORPCClient } from "@orpc/client";
      
      export function createRemoteClient(baseUrl: string, authToken?: string) {
        return createORPCClient(new HTTPRPCLink({
          url: `${baseUrl}/orpc`,
          headers: authToken ? { Authorization: `Bearer ${authToken}` } : undefined,
        }));
      }
    • Reconnect behavior: remote subscriptions (streams) should resubscribe on disconnect using exponential backoff + jitter until the caller’s AbortSignal aborts.

    • Note: the server also exposes a WebSocket endpoint at ${baseUrl}/orpc/ws (see src/browser/contexts/API.tsx) if HTTP streaming proves unreliable through proxies.

  2. Add a filesystem-safe namespacing scheme for remote workspace IDs.

    • New file: src/common/utils/remoteMuxIds.ts

    • Requirements:

      • no : or / (Windows + session dir safety)
      • reversible decode
    • Example:

      // rmux_<serverId>_<remoteWorkspaceId>
      export function encodeRemoteWorkspaceId(serverId: string, remoteId: string): string;
      export function decodeRemoteWorkspaceId(id: string): { serverId: string; remoteId: string } | null;
      export function isRemoteWorkspaceId(id: string): boolean;
  3. Implement rewrite helpers for:

    • FrontendWorkspaceMetadata (id, parentWorkspaceId, projectPath/projectName)
    • WorkspaceChatMessage events (rewrite all ID fields)
    • WorkspaceStatsSnapshot (has workspaceId)
    • workspace.activity records (map keys)

    Why this matters: WorkspaceChatMessageSchema contains fields like:

    • workspaceId in almost every event
    • taskId in task-created
    • sourceWorkspaceId in session-usage-delta

    If these aren’t rewritten, the UI won’t be able to correlate events with the correct workspace in its stores.

Phase 3 — Backend proxying in the existing API surface

Core idea: local src/node/orpc/router.ts detects remote workspace IDs and delegates to the right remote client.

Proxy coverage checklist (MVP)

Treat a workspace as remote if decodeRemoteWorkspaceId(workspaceId) succeeds.

Must proxy for a usable remote agent loop

  • workspace.onChat (and rewrite IDs inside every event)
  • workspace.sendMessage
  • workspace.answerAskUserQuestion (Ask-mode Q&A)
  • workspace.resumeStream / workspace.interruptStream
  • workspace.list (so remote workspaces appear in UI)
  • workspace.onMetadata (UI uses this for real-time updates)
  • workspace.activity.list + workspace.activity.subscribe (sidebar streaming indicator)

Likely needed for basic UI navigation (confirm via a quick grep of UI callsites during implementation)

  • workspace.getInfo
  • workspace.getFullReplay

Defer unless UI breaks

  • workspace.getPlanContent, workspace.getPostCompactionState, workspace.stats.*, workspace.backgroundBashes.*, terminal.*
  • general.openInEditor (remote paths don’t exist locally; probably show an explicit error)
  1. Merge remote workspaces into workspace.list.

    • File: src/node/orpc/router.ts (workspace.list handler)

    • Steps:

      • read local workspaces via context.workspaceService.list()
      • for each enabled remote server:
        • call remote workspace.list({ archived })
        • filter to remote workspaces whose projectPath is in the server’s projectMappings
        • rewrite IDs + map remote projectPath -> localProjectPath
      • return merged list
    • Reliability: wrap each remote fetch in a short timeout; on failure, log + return local workspaces (do not block UI render). Track per-server status for display in Settings.

  2. Proxy workspace.onChat.

    • If decodeRemoteWorkspaceId(input.workspaceId):
      • call remote.workspace.onChat({ workspaceId: remoteId })
      • for await yield rewritten events
    • On remote disconnect/error: let the iterator end/throw so the renderer’s existing onChat retry loop can resubscribe (remote onChat replays full history and emits caught-up).
  3. Proxy workspace mutations required for a functioning remote agent loop:

    • workspace.sendMessage
    • workspace.answerAskUserQuestion
    • workspace.interruptStream / resumeStream
    • plus any endpoints the UI calls unconditionally when opening a workspace (getInfo, etc.)
  4. Multiplex global subscriptions:

    • workspace.onMetadata (merge local + each remote’s workspace.onMetadata)
    • workspace.activity.subscribe + workspace.activity.list
    • Reconnect: run each remote subscription in a retry loop (exponential backoff + jitter). A remote disconnect should not take down the merged iterator—keep local + other remotes streaming while the failed remote retries.

    Implementation sketch for multiplexing:

    const { push, iterate } = createAsyncMessageQueue<...>();
    // start local listener → push
    // start remote subscription tasks → push
    yield* iterate();
  5. Add remote workspace creation (UI-facing API).

    • Add remoteServers.workspace.create procedure:
      • input: { serverId, localProjectPath, branchName, trunkBranch?, title?, runtimeConfig?, sectionId? }
      • backend maps localProjectPath -> remoteProjectPath using the server config
      • calls remote workspace.create({ projectPath: remoteProjectPath, ... })
      • rewrites returned metadata IDs + projectPath back to local

    This avoids overloading workspace.create with a “serverId” field, keeping local creation unchanged.

Phase 4 — Frontend UI updates

  1. Remote server management UI.

    • Add a Settings panel or modal to:
      • add/edit/remove remote servers
      • configure per-project mapping (current local project → remote project path)
      • show connection status (ping)
  2. Workspace creation UI: add “Remote Mux server” option.

    • Files:
      • src/browser/components/ChatInput/CreationControls.tsx
      • src/browser/components/ChatInput/useCreationWorkspace.ts
    • Flow:
      • choose Local vs Remote
      • if Remote: pick server
      • fetch remote runtime availability via backend (can be piggy-backed into a remoteServers.getProjectContext call)
      • create via remoteServers.workspace.create
  3. Remote indicators + guardrails.

    • Add isRemoteWorkspaceId() helper usage to disable local-only UX affordances.
    • Example: custom editor command should not attempt to open a remote path locally.

Phase 5 — Testing

  1. Unit tests

    • ID codec (round-trip, invalid inputs)
    • stream event rewriting (at least: stream-start, task-created, session-usage-delta)
  2. Integration tests

    • Spin up an in-process remote createOrpcServer() (pattern in src/cli/server.test.ts).
    • Configure local backend to point at it.
    • Assert:
      • merged workspace.list returns a rewritten remote ID
      • workspace.onChat yields events whose workspaceId matches the rewritten ID

Operational requirements (remote host)

  • Run mux server on an always-on machine with a persistent MUX_ROOT.
  • Configure provider credentials on the remote host (since the agent loop runs there).
  • Expose the server over network (HTTP), ideally behind TLS.
  • Set --auth-token / MUX_SERVER_AUTH_TOKEN.
  • Ensure long-lived streaming connections are supported (timeouts, reverse proxy settings). Mux will also automatically retry/resubscribe if a connection is dropped, but correct proxy settings reduce churn.
Recommended initial rollout
  1. Start with a single remote server + a single mapped project (to keep the merge logic simple).
  2. Add a “connection degraded” state in the UI if the remote server is unreachable.
  3. After the core loop is stable, expand proxy coverage (terminal, background bashes, MCP, etc.).

Generated with mux • Model: openai:gpt-5.2 • Thinking: xhigh • Cost: $29.98

@github-actions github-actions bot added the enhancement New feature or functionality label Feb 3, 2026
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8aa2ba726

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ThomasK33 ThomasK33 force-pushed the feat/remote-mux-server branch from 25a0e96 to 8247d26 Compare February 4, 2026 11:48
Remote workspaces can have paths that refer to a different machine. Avoid generating local editor deep links unless the workspace is an SSH runtime.

---

_Generated with `mux` • Model: `openai:gpt-5.2` • Thinking: `xhigh` • Cost: ``_

<!-- mux-attribution: model=openai:gpt-5.2 thinking=xhigh costs=unknown -->
@ThomasK33 ThomasK33 force-pushed the feat/remote-mux-server branch from 9a8874b to 0611881 Compare February 4, 2026 14:27
Add remoteServers.listRemoteProjects, which proxies a remote mux server's
projects.list and returns lightweight { path, label } suggestions for UI pickers.
- Hide add/edit forms by default; show the add form only after clicking Add.
- Inline edit within the selected server card (no separate bottom editor).
- Clarify auth token storage: local ~/.mux/secrets.json vs server-side ~/.mux/server.lock.
- Add local/remote project path suggestions for project mappings.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant