Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c863d71
cloud: start an agent session on a Cloud machine from the CLI or an a…
teamleaderleo Oct 26, 2026
6a8a721
cloud start-session: answer pending before the person deadline; remov…
teamleaderleo Oct 26, 2026
8fae4c3
cloud start-session: the card stays its ask's until the run ends; a p…
teamleaderleo Oct 26, 2026
c8f6bf6
cloud start-session: keep a run only when every caller answered pendi…
teamleaderleo Oct 26, 2026
3f6bf32
cloud start-session tests: record the issue, then return
teamleaderleo Oct 26, 2026
7580b09
cloud start-session: pending answers say pending; agents in a Cloud m…
teamleaderleo Oct 26, 2026
c5b97fc
cloud start-session: agent runs follow the outer run's origin; --name…
teamleaderleo Oct 26, 2026
bbff450
cloud start-session: all 21 languages; the pending test reads the tex…
teamleaderleo Oct 26, 2026
1b91960
Merge remote-tracking branch 'origin/feat-cmux-next' into p0-machines…
teamleaderleo Oct 26, 2026
0e9b0fa
cmux next: regenerate action exports with cloud start-session
teamleaderleo Oct 26, 2026
43e9ba1
cloud start-session: test the unread-list refusal end to end and one …
teamleaderleo Oct 26, 2026
b7ae8de
cloud start-session: count creations in a Box
teamleaderleo Oct 26, 2026
8c0c22a
cloud start-session: --name needs this account's list read just now; …
teamleaderleo Oct 26, 2026
cc14c9f
cloud start-session: a run checks the account it was asked in before …
teamleaderleo Oct 26, 2026
376af94
Merge remote-tracking branch 'origin/feat-cmux-next' into p0-machines…
teamleaderleo Oct 26, 2026
ce84d45
cmux next: regenerate action exports with cloud start-session on curr…
teamleaderleo Oct 26, 2026
1f2c5bb
cloud start-session: a --target must be in the list of the account si…
teamleaderleo Oct 26, 2026
a97ce38
cloud start-session: a run's end resumes its callers (no waiter task)…
teamleaderleo Oct 26, 2026
3f7ed6a
cloud start-session: import CmuxNextCompat for the card's account watch
teamleaderleo Oct 26, 2026
40a06f7
cmux next: the agent ask and runs registry behind cloud start-session
teamleaderleo Oct 26, 2026
f639fe0
cloud start-session: build on the agent ask and runs registry (#25414)
teamleaderleo Oct 26, 2026
8f91ab5
cloud start-session asks: label the test helper's key
teamleaderleo Oct 26, 2026
b6fe3ce
Merge remote-tracking branch 'origin/p0-machines/ergo-cloud-start-ses…
teamleaderleo Oct 26, 2026
d7a94b7
cloud start-session asks: endAll walks a snapshot; test an end at the…
teamleaderleo Oct 26, 2026
2c10474
cloud start-session asks: the deadline test waits for the run's end
teamleaderleo Oct 26, 2026
31c8dd9
cloud start-session: check the account before each change on the machine
teamleaderleo Oct 26, 2026
327a53a
Merge remote-tracking branch 'origin/p0-machines/ergo-cloud-start-ses…
teamleaderleo Oct 26, 2026
bb220d1
cloud start-session asks: an ended account wins over a buffered answer
teamleaderleo Oct 26, 2026
9c809cd
cloud start-session: fix the new tests' isolation; a run stopped afte…
teamleaderleo Oct 26, 2026
c2efc27
Merge remote-tracking branch 'origin/p0-machines/ergo-cloud-start-ses…
teamleaderleo Oct 26, 2026
4e0becc
cloud start-session asks: a creation that ends after forgetAll hands …
teamleaderleo Oct 26, 2026
6a1a5ee
cloud start-session: a cancelled or account-ended connect wait is not…
teamleaderleo Oct 26, 2026
81d1189
Merge remote-tracking branch 'origin/p0-machines/ergo-cloud-start-ses…
teamleaderleo Oct 26, 2026
89c3db2
cloud start-session: a step that failed after cancellation or an acco…
teamleaderleo Oct 26, 2026
2d353a9
cloud start-session: a person's gesture is the outermost run's, passe…
teamleaderleo Oct 26, 2026
4a8efc9
cloud start-session asks: mark the caller's one-shot deadline for the…
teamleaderleo Oct 26, 2026
5de27f0
cloud start-session: closing the tab during its start says the tab cl…
teamleaderleo Oct 26, 2026
93c9145
Merge remote-tracking branch 'origin/p0-machines/ergo-cloud-start-ses…
teamleaderleo Oct 26, 2026
7c25736
Merge remote-tracking branch 'origin/feat-cmux-next' into p0-machines…
teamleaderleo Oct 26, 2026
4797152
cloud start-session: regenerate the action list after taking base
teamleaderleo Oct 26, 2026
4da4779
cloud start-session: no async closure default argument (cx-bsue); doc…
teamleaderleo Oct 26, 2026
f8ebfb9
cloud start-session: translate Agent and Start where they matched Eng…
teamleaderleo Oct 26, 2026
d2b5272
Merge remote-tracking branch 'origin/feat-cmux-next' into p0-machines…
teamleaderleo Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Packages/macOS/CmuxNext/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,7 @@ let packageTargets: [Target] = uiLayerPreviewTargets + [
.process("HistoryActions.xcstrings"),
.process("HangReportsActions.xcstrings"),
.process("ImportActions.xcstrings"),
.process("CloudSessionActions.xcstrings"),
.process("MusicActions.xcstrings"),
.process("LayoutActions.xcstrings"),
.process("NewTabActions.xcstrings"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public nonisolated enum ActionCatalog {
CloudPortActionCatalog.self,
AccountActionCatalog.self,
RemoteActionCatalog.self,
CloudSessionActionCatalog.self,
MachineIconActionCatalog.self,
SettingsActionCatalog.self,
DensityActionCatalog.self,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// `cmux cloud start-session` (Lawrence: "start new VMs from acp chat"): take a
// Cloud machine (or create a named one), start an agent session in that
// machine's own acpmux, and answer `{machine_id, session_id, host}`. Titles
// live in CloudSessionActions.xcstrings; the handler is CloudStartSession.swift.

nonisolated enum CloudSessionActionCatalog: ActionCatalogGroup {
static func descriptors() -> [ActionDescriptor] {
var start = ActionDescriptor(
id: "cloudStartSession",
title: String(localized: "action.cloudStartSession", defaultValue: "Start Agent Session on Cloud Machine…", table: "CloudSessionActions", bundle: .module),
description: "Starts an agent session (--agent claude, codex, ...) in the Cloud machine's own acpmux and returns machine_id, session_id and host, plus the chat tab's workspace_id and surface_id. Name the machine with --target machine:<id>, or --name <name> to take that machine or create it; --cwd is the folder there. An agent's run asks the person in its chat first. A run still starting after about 3 minutes answers pending: true; the same command again returns its session.",
keywords: ["vm", "cloud", "agent", "session", "chat", "acp", "acpmux", "start", "claude", "codex"], category: .cloud, symbol: "sparkles",
surfaces: [.palette, .contextMenu], arguments: [agent, name, cwd], targets: [.machine], cliName: "cloud start-session",
surfacePlan: ActionSurfacePlan(cli: .offered, contextMenus: [ActionSurfaceCatalog.p(.cloudMachine, .connection, 221, folder: .connection)])
)
// An agent's run waits for the person's Start on its chat's card, then for the session.
start.waitsForPerson = true
return [start]
}

/// `--agent`: the harness to start (`claude`, `codex`, ...), as the machine's acpmux names it.
static var agent: ActionArgument {
ActionArgument(name: "agent", title: String(localized: "argument.cloudStartSession.agent", defaultValue: "Agent", table: "CloudSessionActions", bundle: .module),
kind: .string)
}

/// `--name`: without `--target`, the machine with this name, created when there is none.
static var name: ActionArgument {
ActionArgument(name: "name", title: String(localized: "argument.cloudStartSession.name", defaultValue: "Machine Name", table: "CloudSessionActions", bundle: .module),
kind: .string, isRequired: false)
}

/// `--cwd`: the folder on the machine the session starts in (default: its workspace's folder).
static var cwd: ActionArgument {
ActionArgument(name: "cwd", title: String(localized: "argument.cloudStartSession.cwd", defaultValue: "Folder on the Machine", table: "CloudSessionActions", bundle: .module),
kind: .string, isRequired: false)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
{
"sourceLanguage": "en",
"strings": {
"action.cloudStartSession": {
"comment": "Command title: starts an agent session (Claude, Codex, ...) on a Cloud machine.",
"extractionState": "manual",
"localizations": {
"en": {"stringUnit": {"state": "translated", "value": "Start Agent Session on Cloud Machine…"}},
"ja": {"stringUnit": {"state": "translated", "value": "Cloud マシンでエージェントセッションを開始…"}},
"de": {"stringUnit": {"state": "needs_review", "value": "Agent-Sitzung auf Cloud-Maschine starten…"}},
"fr": {"stringUnit": {"state": "needs_review", "value": "Démarrer une session d'agent sur une machine Cloud…"}},
"es": {"stringUnit": {"state": "needs_review", "value": "Iniciar sesión de agente en una máquina de Cloud…"}},
"ar": {"stringUnit": {"state": "needs_review", "value": "بدء جلسة وكيل على جهاز Cloud…"}},
"ko": {"stringUnit": {"state": "needs_review", "value": "Cloud 머신에서 에이전트 세션 시작…"}},
"zh-Hans": {"stringUnit": {"state": "needs_review", "value": "在 Cloud 机器上启动代理会话…"}},
"zh-Hant": {"stringUnit": {"state": "needs_review", "value": "在 Cloud 機器上啟動代理工作階段…"}},
"bs": {"stringUnit": {"state": "needs_review", "value": "Pokreni sesiju agenta na Cloud mašini…"}},
"da": {"stringUnit": {"state": "needs_review", "value": "Start agentsession på Cloud-maskine…"}},
"it": {"stringUnit": {"state": "needs_review", "value": "Avvia sessione agente sulla macchina Cloud…"}},
"km": {"stringUnit": {"state": "needs_review", "value": "ចាប់ផ្ដើមវគ្គភ្នាក់ងារនៅលើម៉ាស៊ីន Cloud…"}},
"nb": {"stringUnit": {"state": "needs_review", "value": "Start agentøkt på Cloud-maskin…"}},
"pl": {"stringUnit": {"state": "needs_review", "value": "Uruchom sesję agenta na maszynie Cloud…"}},
"pt-BR": {"stringUnit": {"state": "needs_review", "value": "Iniciar Sessão de Agente na Máquina do Cloud…"}},
"ru": {"stringUnit": {"state": "needs_review", "value": "Запустить сеанс агента на машине Cloud…"}},
"th": {"stringUnit": {"state": "needs_review", "value": "เริ่มเซสชันเอเจนต์บนเครื่อง Cloud…"}},
"tr": {"stringUnit": {"state": "needs_review", "value": "Cloud Makinesinde Ajan Oturumu Başlat…"}},
"uk": {"stringUnit": {"state": "needs_review", "value": "Запустити сеанс агента на машині Cloud…"}},
"vi": {"stringUnit": {"state": "needs_review", "value": "Bắt đầu phiên tác tử trên máy Cloud…"}}
}
},
"argument.cloudStartSession.agent": {
"comment": "Argument title: the agent (harness) to start, such as claude or codex.",
"extractionState": "manual",
"localizations": {
"en": {"stringUnit": {"state": "translated", "value": "Agent"}},
"ja": {"stringUnit": {"state": "translated", "value": "エージェント"}},
"de": {"stringUnit": {"state": "needs_review", "value": "KI-Agent"}},
"fr": {"stringUnit": {"state": "needs_review", "value": "Agent IA"}},
"es": {"stringUnit": {"state": "needs_review", "value": "Agente"}},
"ar": {"stringUnit": {"state": "needs_review", "value": "الوكيل"}},
"ko": {"stringUnit": {"state": "needs_review", "value": "에이전트"}},
"zh-Hans": {"stringUnit": {"state": "needs_review", "value": "代理"}},
"zh-Hant": {"stringUnit": {"state": "needs_review", "value": "代理"}},
"bs": {"stringUnit": {"state": "needs_review", "value": "AI agent"}},
"da": {"stringUnit": {"state": "needs_review", "value": "AI-agent"}},
"it": {"stringUnit": {"state": "needs_review", "value": "Agente"}},
"km": {"stringUnit": {"state": "needs_review", "value": "ភ្នាក់ងារ"}},
"nb": {"stringUnit": {"state": "needs_review", "value": "KI-agent"}},
"pl": {"stringUnit": {"state": "needs_review", "value": "Agent AI"}},
"pt-BR": {"stringUnit": {"state": "needs_review", "value": "Agente"}},
"ru": {"stringUnit": {"state": "needs_review", "value": "Агент"}},
"th": {"stringUnit": {"state": "needs_review", "value": "เอเจนต์"}},
"tr": {"stringUnit": {"state": "needs_review", "value": "Ajan"}},
"uk": {"stringUnit": {"state": "needs_review", "value": "Агент"}},
"vi": {"stringUnit": {"state": "needs_review", "value": "Tác tử"}}
}
},
"argument.cloudStartSession.name": {
"comment": "Argument title: the Cloud machine's name; a machine with that name is created when none exists.",
"extractionState": "manual",
"localizations": {
"en": {"stringUnit": {"state": "translated", "value": "Machine Name"}},
"ja": {"stringUnit": {"state": "translated", "value": "マシン名"}},
"de": {"stringUnit": {"state": "needs_review", "value": "Maschinenname"}},
"fr": {"stringUnit": {"state": "needs_review", "value": "Nom de la machine"}},
"es": {"stringUnit": {"state": "needs_review", "value": "Nombre de la máquina"}},
"ar": {"stringUnit": {"state": "needs_review", "value": "اسم الجهاز"}},
"ko": {"stringUnit": {"state": "needs_review", "value": "머신 이름"}},
"zh-Hans": {"stringUnit": {"state": "needs_review", "value": "机器名称"}},
"zh-Hant": {"stringUnit": {"state": "needs_review", "value": "機器名稱"}},
"bs": {"stringUnit": {"state": "needs_review", "value": "Naziv mašine"}},
"da": {"stringUnit": {"state": "needs_review", "value": "Maskinnavn"}},
"it": {"stringUnit": {"state": "needs_review", "value": "Nome della macchina"}},
"km": {"stringUnit": {"state": "needs_review", "value": "ឈ្មោះម៉ាស៊ីន"}},
"nb": {"stringUnit": {"state": "needs_review", "value": "Maskinnavn"}},
"pl": {"stringUnit": {"state": "needs_review", "value": "Nazwa maszyny"}},
"pt-BR": {"stringUnit": {"state": "needs_review", "value": "Nome da Máquina"}},
"ru": {"stringUnit": {"state": "needs_review", "value": "Имя машины"}},
"th": {"stringUnit": {"state": "needs_review", "value": "ชื่อเครื่อง"}},
"tr": {"stringUnit": {"state": "needs_review", "value": "Makine Adı"}},
"uk": {"stringUnit": {"state": "needs_review", "value": "Назва машини"}},
"vi": {"stringUnit": {"state": "needs_review", "value": "Tên máy"}}
}
},
"argument.cloudStartSession.cwd": {
"comment": "Argument title: the folder on the Cloud machine the agent session starts in.",
"extractionState": "manual",
"localizations": {
"en": {"stringUnit": {"state": "translated", "value": "Folder on the Machine"}},
"ja": {"stringUnit": {"state": "translated", "value": "マシン上のフォルダ"}},
"de": {"stringUnit": {"state": "needs_review", "value": "Ordner auf der Maschine"}},
"fr": {"stringUnit": {"state": "needs_review", "value": "Dossier sur la machine"}},
"es": {"stringUnit": {"state": "needs_review", "value": "Carpeta en la máquina"}},
"ar": {"stringUnit": {"state": "needs_review", "value": "المجلد على الجهاز"}},
"ko": {"stringUnit": {"state": "needs_review", "value": "머신의 폴더"}},
"zh-Hans": {"stringUnit": {"state": "needs_review", "value": "机器上的文件夹"}},
"zh-Hant": {"stringUnit": {"state": "needs_review", "value": "機器上的資料夾"}},
"bs": {"stringUnit": {"state": "needs_review", "value": "Folder na mašini"}},
"da": {"stringUnit": {"state": "needs_review", "value": "Mappe på maskinen"}},
"it": {"stringUnit": {"state": "needs_review", "value": "Cartella sulla macchina"}},
"km": {"stringUnit": {"state": "needs_review", "value": "ថតនៅលើម៉ាស៊ីន"}},
"nb": {"stringUnit": {"state": "needs_review", "value": "Mappe på maskinen"}},
"pl": {"stringUnit": {"state": "needs_review", "value": "Folder na maszynie"}},
"pt-BR": {"stringUnit": {"state": "needs_review", "value": "Pasta na Máquina"}},
"ru": {"stringUnit": {"state": "needs_review", "value": "Папка на машине"}},
"th": {"stringUnit": {"state": "needs_review", "value": "โฟลเดอร์บนเครื่อง"}},
"tr": {"stringUnit": {"state": "needs_review", "value": "Makinedeki Klasör"}},
"uk": {"stringUnit": {"state": "needs_review", "value": "Папка на машині"}},
"vi": {"stringUnit": {"state": "needs_review", "value": "Thư mục trên máy"}}
}
}
},
"version": "1.0"
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ nonisolated struct AgentTabRemoteHost: AgentPaneHostProviding {
let started: AgentTabRemoteStart.Started
do {
started = try await Self.keyedStart(cwd, harness, approvals: approvals,
servesKeyedReason: servesKeyedReason, start: start)
servesKeyedReason: servesKeyedReason, proceed: Self.proceedAlways, start: start)
} catch let refusal as AgentRemoteStartRefusal {
throw AgentSessionRemoteError(code: refusal.code, message: servesTrust ? refusal.userMessage : refusal.withoutTrust.userMessage, folder: refusal.folder)
}
Expand All @@ -86,18 +86,37 @@ nonisolated struct AgentTabRemoteHost: AgentPaneHostProviding {
return handshake
}

/// A start with no step to check first (the page's own first send). A named function, not a
/// default `{}`: an async closure default argument can overrun its context (cx-bsue).
@Sendable static func proceedAlways() async throws {}

/// Starts the new chat now, as the page's first send would (`cmux cloud start-session`): the
/// same keyed start, in `cwd` (nil: the tab's seed folder, then its workspace's) with `harness`.
/// `proceed` runs before each step that changes that machine (a keyed restart, each start
/// try): its throw stops the start there, and what it threw is what this throws.
func startNow(cwd: String?, harness: String, proceed: @escaping @Sendable () async throws -> Void) async throws -> AgentTabRemoteStart.Started {
guard let start else { throw AgentRemoteStartRefusal.startUnsupported(machine: machine) }
return try await Self.keyedStart(cwd, harness, approvals: approvals, servesKeyedReason: servesKeyedReason, proceed: proceed, start: start)
}

/// The chat's Allow needs this app's key on that machine's agent daemon (cx-oa6ah): an idle
/// daemon gets it before the start; a busy one waits for the person's restart click on the
/// Allow notice. That machine's cmux-tui decides whether a restart loses nothing (cx-0w0jj):
/// the start asks for this app's key, and its `needs_keyed_start` answer says whether to
/// restart; after the restart (or without one) the chat starts as it is.
private static func keyedStart(
_ cwd: String?, _ harness: String?, approvals: AgentPaneRemoteApprovals?, servesKeyedReason: Bool,
start: @escaping @Sendable (String?, String?, AgentSessionAttachClient.PersonKey?) async throws -> AgentTabRemoteStart.Started
proceed: @escaping @Sendable () async throws -> Void,
start unchecked: @escaping @Sendable (String?, String?, AgentSessionAttachClient.PersonKey?) async throws -> AgentTabRemoteStart.Started
) async throws -> AgentTabRemoteStart.Started {
let start = { @Sendable (cwd: String?, harness: String?, key: AgentSessionAttachClient.PersonKey?) in
try await proceed()
return try await unchecked(cwd, harness, key)
}
guard let approvals, let prepare = approvals.prepare else { return try await start(cwd, harness, nil) }
guard servesKeyedReason else {
// An older cmux-tui: the app's own status check (remove with ``AgentPaneRemoteApprovals/Preparation/checkStatus``).
try await proceed()
_ = await prepare(.checkStatus)
return try await start(cwd, harness, nil)
}
Expand All @@ -109,7 +128,10 @@ nonisolated struct AgentTabRemoteHost: AgentPaneHostProviding {
// (nil: a restart runs or failed; prepare joins it or tries again)
let current = AcpmuxRemotePersonKeys.shared.instance(machine: approvals.machineID)
let rebound = current != nil && current != key.instance
if refusal.keyedRestartSafe == true, !rebound { _ = await prepare(.daemonSaidSafe) }
if refusal.keyedRestartSafe == true, !rebound {
try await proceed()
_ = await prepare(.daemonSaidSafe)
}
return try await start(cwd, harness, nil)
}
}
Expand Down
Loading
Loading