Skip to content

resume: hookless directory-scoped continue bindings for remote agents (#7989) - #10049

Open
alloevil wants to merge 1 commit into
manaflow-ai:mainfrom
alloevil:feat-7989-hookless-remote-resume
Open

resume: hookless directory-scoped continue bindings for remote agents (#7989)#10049
alloevil wants to merge 1 commit into
manaflow-ai:mainfrom
alloevil:feat-7989-hookless-remote-resume

Conversation

@alloevil

@alloevil alloevil commented Aug 12, 2026

Copy link
Copy Markdown

Implements the Tier-1 flavor proposed in #7989's discussion: synthesize a directory-scoped continue binding for remote agents that never published a hook binding, so a persistent-SSH restore can resume the agent after the remote PTY is genuinely gone — without requiring cmux hooks setup on every remote host.

Problem

Local process discovery can't see agents on the SSH host, and hook-published bindings require per-host, per-agent installation — a real adoption cliff for multi-host users. Result: restore reattaches the PTY, but a genuinely-gone PTY leaves the agent unresumed (resume_binding: null).

Approach

When the snapshot still records the agent kind + remote working directory (and wasAgentRunning), RemoteAgentContinueSynthesizer builds a binding from inline templates:

kind command
claude cd -- '<dir>' … && claude --continue || claude
codex cd -- '<dir>' … && codex resume --last || codex
all others nothing (no trustworthy sessionless continue invocation)

Design points, mapped to the issue's acceptance criteria:

  • Live PTY → attach-only, never a duplicate agent (criterion 2): rides the existing requireExisting pipeline; the synthesized command is additionally hard-gated in reattachPersistentRemotePTYPanels to inject only once the PTY is confirmed ended. A directory-scoped continue has no session checkpoint the remote once-guard could reconcile against, so this gate is stricter than for hook bindings.
  • Gone PTY → command executes on the remote host, never locally (criterion 3): binding carries .persistentSSH(SurfaceResumeRemoteContext) — the existing flavor, no new Codable case — and enters through the SSH persistent-session machinery via remotePTYAttachStartupCommand. No wrapper-resolver tokens (they resolve local Mac paths), mirroring remoteStartupInput()'s repairPortableAgentExecutable: false convention.
  • Trust model: new source remote-synthesized shares the process-detected tier (bypasses the signed approval store) because the command is built exclusively from cmux's own inline templates with no caller-supplied arguments — an observation-grade artifact, not a proposal from an arbitrary process. The gate for every other source is unchanged (covered by a regression test).
  • Precedence: agent-hook / cli / process-detected bindings always win; synthesis only fills the gap, and respects the auto-resume setting.

Known limitations (documented in code + tests)

  • Directory-scoped continue resumes the most recent session in that directory: a directory shared by same-kind agents can continue the wrong conversation. Hook bindings (Tier 2, this issue's original design) remain the precise path and always take precedence — the two tiers compose rather than compete.
  • Persistent-SSH workspaces only; plain SSH panes have no reattach seam to hang the liveness gate on.
  • Dock restore path has no remote-PTY seam today; left as follow-up rather than duplicating workspace machinery.

Tests

RemoteAgentContinueSynthesizerTests (15 cases, Swift Testing): exact command form per kind, nil for the 16 uncovered kinds + custom (parameterized), cwd guards, single-quote splice injection safety, non-ASCII printf-octal quoting, isRemoteSynthesized predicate + mutual exclusion across all four sources (parameterized), trust-tier resolution with a .pending signing secret, cli-source demotion regression guard, reconcile pass-through, remoteStartupInput() verbatim replay, Codable round-trip with quote-bearing cwd.

Caveats

  • Developed on Linux against main; every call was verified against the callee's source, but I could not compile or run the app locally. Happy to iterate on anything the macOS build or the full restore matrix surfaces.
  • xcodeproj wiring for the 2 new files is included (lint-pbxproj-test-wiring passes).

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Synthesizes directory-scoped resume bindings for hookless remote agents in persistent-SSH workspaces, so restores can resume agents after the remote PTY ends without installing hooks on every host. Previously, restore reattached the PTY but left the agent unresumed when the PTY was gone.

  • Adds remote-synthesized bindings for claude (“claude --continue || claude”) and codex (“codex resume --last || codex”) only when no binding exists, the snapshot retains agent kind and remote cwd, the workspace is persistent-SSH, and auto-resume is enabled; other kinds synthesize nothing.
  • Executes the command on the remote host via .persistentSSH(...); remoteStartupInput() replays the stored command verbatim; UI maps to .direct (no checkpoint).
  • Injects the command only after the remote PTY is confirmed ended; a live PTY is attach-only to avoid duplicate agents.
  • remote-synthesized shares the trust tier of process-detected bindings and bypasses the signed approval store; all other sources are unchanged.
  • Precedence: agent-hook/cli/process-detected bindings win over synthesis.
  • Does not replay a previously persisted synthesized binding when auto-resume is disabled.
  • Touch points: new RemoteAgentContinueSynthesizer.swift; trust tier in SurfaceResumeApprovalSigningSecretCache; isRemoteSynthesized in SessionPersistence; PTY gating in Workspace+PersistentRemotePTYReattach; auto-resume gating in Workspace+RemoteSurfaceResumeBinding; direct-mode mapping in ControlSurfaceResumeTarget; synthesis and startup selection in Workspace; tests and Xcode project wiring.

Limitations

  • Directory-scoped continue resumes the most recent session for that directory and can pick the wrong conversation if multiple same-kind agents share the cwd.
  • Works only with persistent-SSH workspaces; plain SSH panes and Dock restore paths are not covered.

Written for commit cb3225c. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Remote terminal sessions can now automatically resume supported Claude and Codex sessions.
    • Resume commands are scoped to the correct remote working directory and persistent SSH session.
    • Live remote sessions attach without injecting duplicate resume commands.
    • Automatic resume settings are consistently honored during restoration.
  • Bug Fixes

    • Improved handling when sessions end, directories are unavailable, or agents are unsupported.
    • Remote-synthesized resume bindings are now trusted and restored consistently.

…mote agents (manaflow-ai#7989)

An agent launched inside a persistent-SSH workspace without relayed
hooks leaves no resume binding, so a restore reattaches the PTY but
never resumes the agent once the PTY is genuinely gone. Requiring
`cmux hooks setup` on every remote host for every agent CLI is a real
adoption cliff for multi-host users.

This adds the Tier-1 flavor sketched in the issue discussion: when the
snapshot still knows the agent kind and remote working directory,
synthesize a directory-scoped continue binding from cmux's own inline
templates (claude -> `claude --continue || claude`, codex ->
`codex resume --last || codex`; kinds with no trustworthy sessionless
continue synthesize nothing).

Design points:

- New binding source `remote-synthesized`, sharing the trust tier of
  process-detected bindings: the command is built exclusively from
  inline templates with no caller-supplied arguments, so it bypasses
  the signed approval store the same way. The gate for every other
  source is unchanged.
- Reuses .persistentSSH(SurfaceResumeRemoteContext) - no new Codable
  case, no persistence-format risk.
- Liveness-gated through the existing requireExisting attach pipeline:
  a live remote PTY is attach-only (the synthesized command is only
  injected once the PTY is confirmed ended), so restore can never race
  a live agent and create a duplicate writing the same directory.
- Precedence: agent-hook / cli / process-detected bindings always win;
  synthesis only fills the gap, and only when the snapshot recorded a
  running agent and auto-resume is enabled.

Known limitation (documented in code): directory-scoped continue
resumes the most recent session in that directory, so a directory
shared by multiple agents of the same kind can continue the wrong
conversation. Hook-published bindings (Tier 2) remain the precise
path and always take precedence.

Refs manaflow-ai#7989.
@cursor

cursor Bot commented Aug 12, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds synthesized remote agent resume bindings for Claude and Codex sessions. Persistent-SSH restoration now creates, trusts, filters, persists, and tests these bindings based on agent state, working directory, auto-resume settings, and PTY liveness.

Changes

Remote agent resume

Layer / File(s) Summary
Synthesize remote continue bindings
Sources/RemoteAgentContinueSynthesizer.swift, Sources/SessionPersistence.swift, Sources/ControlSurfaceResumeTarget.swift
Supported agents receive directory-scoped continue commands with SSH metadata. Synthesized bindings expose a dedicated source predicate and direct-mode behavior.
Integrate bindings into restoration
Sources/Workspace.swift, Sources/SurfaceResumeApprovalSigningSecretCache.swift, Sources/Workspace+RemoteSurfaceResumeBinding.swift, Sources/Workspace+PersistentRemotePTYReattach.swift
Restoration creates eligible synthesized bindings, trusts them automatically, suppresses commands for live PTYs, and records startup-command state.
Validate and compile the restore path
cmuxTests/RemoteAgentContinueSynthesizerTests.swift, cmux.xcodeproj/project.pbxproj
Tests cover command generation, quoting, trust, restoration, startup input, and Codable persistence. The new source and test files are added to the Xcode targets.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Workspace
  participant RemoteAgentContinueSynthesizer
  participant ApprovalCache
  participant PersistentSSHPTY
  Workspace->>RemoteAgentContinueSynthesizer: synthesize binding for eligible remote agent
  RemoteAgentContinueSynthesizer-->>Workspace: return continue command and SSH metadata
  Workspace->>ApprovalCache: evaluate synthesized binding trust
  Workspace->>PersistentSSHPTY: check PTY liveness
  PersistentSSHPTY-->>Workspace: return attach command
Loading

Possibly related issues

Possibly related PRs

  • manaflow-ai/cmux#8593 — Modifies persistent-SSH agent restoration and startup resume-state handling.
  • manaflow-ai/cmux#9855 — Modifies remote agent restoration and resume binding behavior in overlapping workspace and persistence code.
  • manaflow-ai/cmux#9964 — Modifies remote agent restoration and resume command handling.

Suggested reviewers: lawrencecchen, austinywang


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (4 errors, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Swift Package Boundaries ❌ Error New Sources/RemoteAgentContinueSynthesizer.swift is app-target production code with Foundation-only, lifecycle-independent logic and extensive isolated unit coverage. Extract the command-selection and directory-quoting core into a small CmuxAgentResume SwiftPM target exposing RemoteAgentContinueCommandProvider; keep binding assembly and Workspace wiring in cmux.
Cmux User-Facing Error Privacy ❌ Error The new remote binding exposes cd ... && claude --continue or codex resume --last through surface.resume command output, revealing vendor names and provider-specific flags. Keep raw remote commands internal for execution. Return a sanitized, generic resume description in user-visible payloads and alerts.
Cmux Full Internationalization ❌ Error New production binding label "\\(kind.displayName) continue" is user-facing but bypasses localization; no matching catalog key or locale entries were added. Route the synthesized label through String(localized:defaultValue:) and add its key with translations for all 20 locales in Resources/Localizable.xcstrings.
Cmux No Ambient Global State ❌ Error Sources/RemoteAgentContinueSynthesizer.swift:17 adds a caseless enum whose API is only static let/functions (lines 20, 25, 64, 80), matching the rule's explicit static-namespace failure. Move synthesis behavior to a constructable RemoteAgentContinueSynthesizer instance, inject it at the Workspace restore seam, and keep only narrowly scoped private helpers.
Docstring Coverage ⚠️ Warning Docstring coverage is 31.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Swiftui State Layout ❓ Inconclusive The working tree has no diff, so the pull-request patch cannot be verified against the SwiftUI state-layout rules. Provide the pull-request base and head revisions or a usable diff.
✅ Passed checks (19 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: hookless, directory-scoped continue bindings for remote agents.
Description check ✅ Passed The description clearly explains the change, rationale, implementation, limitations, and tests, though template process sections and a demo video are absent.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed The diff adds a top-level pure helper and a computed property on an existing Sendable value model; Workspace access remains @MainActor, with no new shared mutable Sendable type or background UI-sto...
Cmux Swift Blocking Runtime ✅ Passed The production diff adds no semaphore, wait, sleep, delayed dispatch, polling, main-queue sync, or manual lock. The Date timestamp only records binding metadata; existing flagged code is unchanged.
Cmux Browser Automation Off-Main ✅ Passed The diff changes remote-agent resume/SSH persistence files and tests only; it adds no browser.* commands, WebKit/page waits, worker routing, or browser policy changes.
Cmux Expensive Synchronous Load ✅ Passed The diff adds no agent-history or file loader; synthesis is pure snapshot-to-command logic, and the existing SharedLiveAgentIndex ?? RestorableAgentSessionIndex.load() fallback is unchanged.
Cmux Cache Substitution Correctness ✅ Passed The diff adds synthesis only when no binding exists; it does not replace a fresh authoritative read with a cache. The approval-cache change only extends trust classification.
Cmux No Hacky Sleeps ✅ Passed The diff changes only Swift sources/tests and Xcode project wiring; it introduces no TypeScript, JavaScript, shell, or non-Swift runtime delay code covered by this check.
Cmux Algorithmic Complexity ✅ Passed The production diff adds only scalar guards, a fixed two-case switch, and O(1) binding lookups; it adds no nested scans or per-target collection rescans.
Cmux Swift Concurrency ✅ Passed The PR diff adds only synchronous binding and restore logic; no new Dispatch queues, Combine state, completion handlers, or fire-and-forget Tasks. Existing async code is unchanged.
Cmux Swift @Concurrent ✅ Passed The diff adds only synchronous binding and string-building logic; it introduces no async, nonisolated, @concurrent, or new async call sites, and the restore caller remains @MainActor.
Cmux Swiftpm Lockfiles ✅ Passed The PR changes Swift sources and source/test entries in cmux.xcodeproj only; it changes no Package.swift, Package.resolved, .gitignore, workflow, or SwiftPM package reference.
Cmux Swift Logging ✅ Passed The Swift diff adds no print, debugPrint, dump, NSLog, Logger, or ad hoc file/stdout logging; existing cmuxDebugLog calls remain DEBUG-guarded and unchanged.
Cmux Architecture Rethink ✅ Passed The diff uses a pure binding synthesizer and existing PTY liveness state; it adds no sleeps, polling, locks, observers, side-channel owner, duplicate behavior path, or UI lifecycle owner.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR adds remote resume logic and tests, not standalone windows; added Swift lines contain no window constructs, and scripts/lint_auxiliary_window_close_shortcuts.py passes.
Cmux Source Artifacts ✅ Passed All nine changed paths are Swift source/tests or required Xcode project entries; the diff adds no logs, binaries, caches, scratch directories, or other artifact-like paths.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The production diff adds no test-build guard or seam-named member; RemoteAgentContinueSynthesizer and isRemoteSynthesized are called by production restore code, while existing DEBUG logging is only...
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Sources/SessionPersistence.swift (1)

380-382: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Include remote-synthesized bindings in detected-binding precedence.

shouldYieldToDetectedSurfaceResumeBinding is used during runtime reconciliation. A stored remote-synthesized binding does not yield to a later process-detected binding, so the less accurate binding remains active. Include isRemoteSynthesized in the yield condition.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/SessionPersistence.swift` around lines 380 - 382, Update
shouldYieldToDetectedSurfaceResumeBinding to also treat isRemoteSynthesized as
yielding to a detected process binding, while preserving the existing
isProcessDetected and isAgentHookBinding checks.
Sources/SurfaceResumeApprovalSigningSecretCache.swift (1)

391-416: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Reject reserved trust sources at the public surface.resume.set boundary.

remote-synthesized passes through publicResumeSource and is copied into the binding. trustedBinding then bypasses signed approval and forces automatic resume. A caller can therefore store an arbitrary command for automatic execution on restore. Accept only internally authenticated provenance values and fail closed for reserved sources.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/SurfaceResumeApprovalSigningSecretCache.swift` around lines 391 -
416, Update trustedBinding to accept only internally authenticated provenance
for process-detected and agent-hook bindings, and reject remote-synthesized or
other caller-supplied reserved sources at the public surface.resume.set
boundary. Ensure untrusted bindings return nil or otherwise fail closed before
bypassing signed approval or enabling automatic resume, while preserving
automatic handling for genuinely internal observations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/SessionPersistence.swift`:
- Around line 380-382: Update shouldYieldToDetectedSurfaceResumeBinding to also
treat isRemoteSynthesized as yielding to a detected process binding, while
preserving the existing isProcessDetected and isAgentHookBinding checks.

In `@Sources/SurfaceResumeApprovalSigningSecretCache.swift`:
- Around line 391-416: Update trustedBinding to accept only internally
authenticated provenance for process-detected and agent-hook bindings, and
reject remote-synthesized or other caller-supplied reserved sources at the
public surface.resume.set boundary. Ensure untrusted bindings return nil or
otherwise fail closed before bypassing signed approval or enabling automatic
resume, while preserving automatic handling for genuinely internal observations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1ba7a8db-534e-40fb-9ff1-94eeab363538

📥 Commits

Reviewing files that changed from the base of the PR and between b17c260 and cb3225c.

📒 Files selected for processing (9)
  • Sources/ControlSurfaceResumeTarget.swift
  • Sources/RemoteAgentContinueSynthesizer.swift
  • Sources/SessionPersistence.swift
  • Sources/SurfaceResumeApprovalSigningSecretCache.swift
  • Sources/Workspace+PersistentRemotePTYReattach.swift
  • Sources/Workspace+RemoteSurfaceResumeBinding.swift
  • Sources/Workspace.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/RemoteAgentContinueSynthesizerTests.swift

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.

1 participant