Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
01a3136
Merge main (85e3c90f441f) into task-cloud-startup-optimization
austinywang Oct 8, 2026
a199bfa
test(cloud): pin create attach contract
austinywang Oct 8, 2026
3d4f323
Merge main (31dd5437b1e2) into task-cloud-startup-optimization
austinywang Oct 8, 2026
1b03483
Merge main (31dd5437b1e) into task-cloud-startup-optimization
austinywang Oct 8, 2026
45b4ad1
perf(cloud): create New Machine in process
austinywang Oct 8, 2026
a46d8e0
Merge main (6003437cce06) into task-cloud-startup-optimization
austinywang Oct 8, 2026
06bdd63
Merge main (a255d8052d52) into task-cloud-startup-optimization
austinywang Oct 8, 2026
a4ee92e
fix(cloud): include token in create attach receipt
austinywang Oct 9, 2026
8bfad3a
fix(cloud): fence cancelled create receipts
austinywang Oct 9, 2026
3df8639
test(cloud): cover upstream errors in create recovery
austinywang Oct 9, 2026
1909b88
fix(cloud): address in-process create review findings
austinywang Oct 9, 2026
d8ede06
fix(cloud): fence provider fallback by creation scope
austinywang Oct 9, 2026
86feaa1
fix(cloud): preserve focused pane during reserved adoption
austinywang Oct 9, 2026
63f3d56
fix(cloud): fence in-process workspace adoption
austinywang Oct 9, 2026
c4ae416
fix(cloud): repair launcher review regressions
austinywang Oct 10, 2026
995a33a
fix(cloud): admit input before workspace discovery
austinywang Oct 10, 2026
8c4fb07
fix(cloud): reuse detached starter terminal
austinywang Oct 10, 2026
18cf862
Merge origin/main into task-cloud-startup-optimization
austinywang Oct 10, 2026
b463a4f
Merge main (42746dd3b8cb) into task-cloud-startup-optimization
austinywang Oct 10, 2026
23e0c32
fix(cloud): preserve create attach session and reservations
austinywang Oct 10, 2026
ea44239
Merge main (bbb3633af923) into task-cloud-startup-optimization
austinywang Oct 10, 2026
dca4845
test(cloud): exercise session forwarding through bundled CLI
austinywang Oct 10, 2026
8c027cd
fix(cloud): satisfy Swift warning budget
austinywang Oct 10, 2026
e1ff481
test(cloud): stabilize hosted UI and CLI fixtures
austinywang Oct 10, 2026
156a179
test(cloud): enable hosted accessibility fixtures
austinywang Oct 10, 2026
afad103
Merge remote-tracking branch 'origin/main' into task-cloud-startup-op…
austinywang Oct 10, 2026
0a42df2
test(cloud): use hosted accessibility frames for machine sheet
austinywang Oct 10, 2026
8006181
test(cloud): order hosted pop-ups by visual row
austinywang Oct 10, 2026
3e181b1
test(cloud): keep native machine sheet controls in fixtures
austinywang Oct 10, 2026
28d4e10
test(cloud): locate hosted machine menus by role
austinywang Oct 10, 2026
351b4a3
test(cloud): match hosted machine menus by labels
austinywang Oct 10, 2026
7b972a2
test(cloud): keep accessibility controls in hosted fixtures
austinywang Oct 10, 2026
d8c254a
Merge main (f7095c984b95) into task-cloud-startup-optimization
austinywang Oct 10, 2026
7e8efc1
test(cloud): reproduce reconnect and stale discovery regressions
austinywang Oct 10, 2026
c2a10d5
fix(cloud): use Cloud session and force post-create discovery
austinywang Oct 10, 2026
40d2ce5
test: stabilize cloud shortcut coverage
austinywang Oct 10, 2026
e88bb60
fix: release closed cloud managers and avoid duplicate tmux panes
austinywang Oct 10, 2026
9cdb34e
test: restore shared cloud fixture binding environment
austinywang Oct 10, 2026
2bca6ba
test: avoid waiting for duplicate cloud creation event
austinywang Oct 10, 2026
898412c
fix: preserve optimistic cloud workspace focus
austinywang Oct 10, 2026
b97dedb
test: keep delayed cloud reveal visible
austinywang Oct 10, 2026
069bca9
fix(cloud): admit unknown graphs and moved workspaces
austinywang Oct 10, 2026
68c8731
docs(cloud): clarify graph discovery fallback
austinywang Oct 10, 2026
7d28bd9
test(cloud): guard unknown graphs and reserved retry identity
austinywang Oct 10, 2026
aba3295
test(cloud): cover pane retries and retain bound identity
austinywang Oct 10, 2026
a84f99a
fix(cloud): retain bound identity and retry moved panes safely
austinywang Oct 10, 2026
ec4b609
test(cloud): register the destination window for moved-pane retries
austinywang Oct 10, 2026
3b2503c
test(cloud): stabilize moved retry and sheet fixture timing
austinywang Oct 10, 2026
31db0b5
Merge main into task-cloud-startup-optimization
austinywang Oct 12, 2026
4b1f6ef
Merge main (02f7edeeccad) into task-cloud-startup-optimization
austinywang Oct 12, 2026
8641a03
Merge main tip into task-cloud-startup-optimization
austinywang Oct 12, 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
63 changes: 61 additions & 2 deletions Packages/macOS/CmuxCloud/Sources/CmuxCloud/VMClient/VMClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,8 @@ public struct VMSummary: Sendable {
addressIPv6: String? = nil,
cmuxTuiContract: String? = nil,
createdBy: VMCreator? = nil,
agentUpdates: CloudAgentUpdates? = nil
agentUpdates: CloudAgentUpdates? = nil,
createAttach: VMCmuxRemoteEndpoint? = nil
) {
self.id = id
self.provider = provider
Expand All @@ -327,6 +328,7 @@ public struct VMSummary: Sendable {
self.cmuxTuiContract = cmuxTuiContract
self.createdBy = createdBy
self.agentUpdates = agentUpdates
self.createAttach = createAttach
}

public func withStatus(_ status: String) -> VMSummary {
Expand All @@ -346,7 +348,8 @@ public struct VMSummary: Sendable {
addressIPv6: addressIPv6,
cmuxTuiContract: cmuxTuiContract,
createdBy: createdBy,
agentUpdates: agentUpdates
agentUpdates: agentUpdates,
createAttach: createAttach
)
}

Expand Down Expand Up @@ -382,6 +385,8 @@ public struct VMSummary: Sendable {
public var cmuxTuiContract: String?
/// Whether the machine keeps its image's coding agents or updates them on attach.
public var agentUpdates: CloudAgentUpdates?
/// A create-only dial receipt from snapshot-v2. List and status responses leave this nil.
public var createAttach: VMCmuxRemoteEndpoint?

/// The name to show people: the label when set, else the generated slug,
/// else the machine id.
Expand Down Expand Up @@ -1490,6 +1495,52 @@ public actor VMClient {
return VMMachineKind(rawValue: raw.lowercased())
}

/// Decodes the optional snapshot-v2 dial receipt carried by `POST /api/vm`.
/// Older control planes omit it and continue through the existing attach path.
static func decodeCreateAttach(_ raw: Any?) -> VMCmuxRemoteEndpoint? {
guard let object = raw as? [String: Any],
object["transport"] as? String == "cmux-remote",
let route = object["route"] as? String,
!route.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
IPNetworkPrefix.routeHost(route) != nil,
let session = object["session"] as? String,
!session.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
else { return nil }
let buildObject = object["daemonBuild"] as? [String: Any]
?? object["daemon_build"] as? [String: Any]
let build = buildObject.map {
VMCmuxRemoteEndpoint.DaemonBuild(
commit: $0["commit"] as? String,
remoteProtocol: ($0["remoteProtocol"] as? Int)
?? ($0["remote_protocol"] as? Int)
?? ($0["remoteProtocol"] as? Double).map(Int.init)
?? ($0["remote_protocol"] as? Double).map(Int.init),
version: $0["version"] as? String
)
}
let addresses = (object["networkAddresses"] ?? object["network_addresses"]) as? [String: Any]
let networkAddresses: VMCmuxRemoteEndpoint.NetworkAddresses?
if let addresses, addresses["ipv4"] is String || addresses["ipv6"] is String {
networkAddresses = .init(ipv4: addresses["ipv4"] as? String, ipv6: addresses["ipv6"] as? String)
} else {
networkAddresses = nil
}
let expiresAtUnix = (object["expiresAtUnix"] as? Int64)
?? (object["expires_at_unix"] as? Int64)
?? Int64((object["expiresAtUnix"] as? Double) ?? (object["expires_at_unix"] as? Double) ?? 0)
return VMCmuxRemoteEndpoint(
route: route,
token: object["token"] as? String ?? "",
expiresAtUnix: expiresAtUnix,
session: session,
trustedCarrier: (object["trustedCarrier"] as? Bool)
?? (object["trusted_carrier"] as? Bool)
?? false,
networkAddresses: networkAddresses,
daemonBuild: build
)
}

/// `limits.imageKinds: [{kind, image}]`; malformed entries are skipped.
static func decodeImageKinds(_ raw: Any?) -> [VMImageKindOption] {
guard let items = raw as? [[String: Any]] else { return [] }
Expand Down Expand Up @@ -1610,6 +1661,14 @@ public actor VMClient {
}
summary.cmuxTuiContract = (obj["cmuxTuiContract"] as? String).flatMap { $0.isEmpty ? nil : $0 }
summary.agentUpdates = CloudAgentUpdates(wireValue: obj["agentUpdates"])
summary.createAttach = Self.decodeCreateAttach(obj["attach"])
?? (summary.cmuxTuiContract == "snapshot-v2" ? Self.decodeCreateAttach([
"transport": "cmux-remote",
"route": summary.addressIPv4.map { "ws://\($0):1337/v1/link" }
?? summary.addressIPv6.map { "ws://[\($0)]:1337/v1/link" } ?? "",
"session": "cloud",
"trustedCarrier": true,
]) : nil)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
machineCache.record(hasAnyMachine: true)
return summary
}
Expand Down
252 changes: 252 additions & 0 deletions Sources/Cloud/InProcessMachineCreateLauncher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import AppKit
import CmuxCloud
import CmuxCloudMachines
import CmuxSurfaceCatalogModel
import Foundation

/// Launches the New Machine flow in-process while retaining the CLI-shaped coordinator contract.
@MainActor
enum InProcessMachineCreateLauncher {
struct Invocation: Equatable, Sendable {
let machineID: String?
let kind: VMMachineKind?
let memoryMb: Int?
let displayName: String?
let networkPolicy: CloudNetworkPolicy?
let agentUpdates: CloudAgentUpdates?
let workspaceID: UUID
let focus: Bool
let windowID: UUID?
}

nonisolated static let placeholderCommand = "sleep 60"

/// Recognizes only the New Machine subset. Base, fork, image overrides, and unknown flags stay on CLI.
nonisolated static func parse(arguments: [String]) -> Invocation? {
guard arguments.count >= 2, arguments[0] == "vm" else { return nil }
let verb = arguments[1]
guard verb == "new" || verb == "open" else { return nil }
if verb == "open" {
guard arguments.count >= 3, !arguments[2].isEmpty, !arguments[2].hasPrefix("-") else { return nil }
var focus = true
var workspaceID: UUID?
var windowID: UUID?
var index = 3
while index < arguments.count {
switch arguments[index] {
case "--focus":
guard index + 1 < arguments.count else { return nil }
switch arguments[index + 1].lowercased() {
case "true", "1", "yes": focus = true
case "false", "0", "no": focus = false
default: return nil
}
index += 1
case "--workspace":
guard index + 1 < arguments.count, let value = UUID(uuidString: arguments[index + 1]) else { return nil }
workspaceID = value; index += 1
case "--window":
guard index + 1 < arguments.count, let value = UUID(uuidString: arguments[index + 1]) else { return nil }
windowID = value; index += 1
default: return nil
}
index += 1
}
guard let workspaceID else { return nil }
return Invocation(
machineID: arguments[2], kind: nil, memoryMb: nil, displayName: nil,
networkPolicy: nil, agentUpdates: nil, workspaceID: workspaceID,
focus: focus, windowID: windowID
)
}
var kind: VMMachineKind?
var memoryMb: Int?
var displayName: String?
var networkPolicy: CloudNetworkPolicy?
var agentUpdates: CloudAgentUpdates?
var focus = true
var workspaceID: UUID?
var windowID: UUID?
var index = 2
while index < arguments.count {
switch arguments[index] {
case "--desktop": kind = .desktop
case "--base", "--no-desktop": return nil
case "--size":
guard index + 1 < arguments.count, let value = Int(arguments[index + 1]), value > 0 else { return nil }
memoryMb = value; index += 1
case "--name":
guard index + 1 < arguments.count else { return nil }
let value = arguments[index + 1].trimmingCharacters(in: .whitespacesAndNewlines)
displayName = value.isEmpty ? nil : value; index += 1
case "--network-policy":
guard index + 1 < arguments.count,
let data = arguments[index + 1].data(using: .utf8),
let object = try? JSONSerialization.jsonObject(with: data),
let policy = try? CloudNetworkPolicy(foundationObject: object) else { return nil }
networkPolicy = policy; index += 1
case "--agent-updates":
guard index + 1 < arguments.count, let value = CloudAgentUpdates(rawValue: arguments[index + 1]) else { return nil }
agentUpdates = value; index += 1
case "--focus":
guard index + 1 < arguments.count else { return nil }
switch arguments[index + 1].lowercased() {
case "true", "1", "yes": focus = true
case "false", "0", "no": focus = false
default: return nil
}
index += 1
case "--workspace":
guard index + 1 < arguments.count, let value = UUID(uuidString: arguments[index + 1]) else { return nil }
workspaceID = value; index += 1
case "--window":
guard index + 1 < arguments.count, let value = UUID(uuidString: arguments[index + 1]) else { return nil }
windowID = value; index += 1
default: return nil
}
index += 1
}
guard let workspaceID else { return nil }
return Invocation(
machineID: nil, kind: kind, memoryMb: memoryMb, displayName: displayName,
networkPolicy: networkPolicy, agentUpdates: agentUpdates,
workspaceID: workspaceID, focus: focus, windowID: windowID
)
}

nonisolated static func idempotencyKey(operationID: UUID) -> String {
"app-" + operationID.uuidString.lowercased()
}

struct Dependencies {
let create: @Sendable (Invocation, String) async throws -> VMSummary
let status: @Sendable (String) async throws -> VMSummary
let record: @MainActor (VMSummary, VMCmuxRemoteEndpoint?) async -> Void
let prepare: @MainActor (UUID) throws -> Void
let bind: @MainActor (UUID, String) -> Void
let connect: @MainActor (String) async throws -> Void
let terminal: @MainActor (String, UUID, Bool) async throws -> String?
}

private static func dependencies(client: VMClient, registry: CmuxTuiSurfaceProviderRegistry) -> Dependencies {
let scope = registry.creationScope
return Dependencies(
create: { invocation, key in
try await client.create(
kind: invocation.kind,
persistentHome: false,
perMachineHome: false,
memoryMb: invocation.memoryMb,
displayName: invocation.displayName,
networkPolicy: invocation.networkPolicy,
agentUpdates: invocation.agentUpdates,
idempotencyKey: key
)
},
status: { id in try await client.status(id: id) },
record: { summary, attach in await registry.recordCreatedMachine(summary, attach: attach, scope: scope) },
prepare: { workspaceID in
guard let workspace = Workspace.liveWorkspace(id: workspaceID),
workspace.panels.values.contains(where: { $0.panelType == .cloudVMLoading }) else {
throw CloudMachineLinkManager.ManagerError.retryLater("The reserved Cloud workspace is no longer available.")
}
(workspace.panels.values.first { $0.panelType == .cloudVMLoading } as? CloudVMLoadingPanel)?.resetLoading()
},
bind: { workspaceID, machineID in
SurfaceCatalog.shared.bindCloudWorkspace(
localWorkspaceID: workspaceID,
machine: .cloud(machineID),
remoteWorkspaceID: nil,
isBase: false,
generatedTitle: String(localized: "workspace.cloudVM.defaultTitle", defaultValue: "Cloud VM")
)
},
connect: { machineID in _ = try await registry.linkSocketPath(machineID: machineID) },
terminal: { machineID, workspaceID, focus in
let payload = try await TerminalController.surfaceNewTerminal(
machine: .cloud(machineID), command: nil, cwd: nil, name: nil,
remoteWorkspaceID: nil,
destination: .workspace(id: workspaceID, placement: .tab),
focus: focus
)
return payload["remote_workspace_id"] as? String
}
)
}

static func run(
_ invocation: Invocation,
operationID: UUID,
dependencies: Dependencies,
onOutput: @escaping @MainActor (String) -> Void
) async -> CloudVMActionLauncher.Completion {
var output = ""
var machineID: String?
do {
if let openedMachineID = invocation.machineID {
machineID = openedMachineID
} else {
var summary = try await dependencies.create(invocation, idempotencyKey(operationID: operationID))
machineID = summary.id
// A create may finish after the coordinator has tombstoned the
// operation. Fence cancellation before admitting its receipt so
// a late completion cannot repopulate another lifecycle.
try Task.checkCancellation()
let receipt = "OK machine=\(summary.id)\n"
output += receipt
onOutput(receipt)
if summary.createAttach?.networkAddresses?.ipv4 != nil || summary.createAttach?.networkAddresses?.ipv6 != nil {
if summary.addressIPv4 == nil { summary.addressIPv4 = summary.createAttach?.networkAddresses?.ipv4 }
if summary.addressIPv6 == nil { summary.addressIPv6 = summary.createAttach?.networkAddresses?.ipv6 }
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
await dependencies.record(summary, summary.createAttach)
}
guard let machineID else { throw VMClientError.malformedResponse("Cloud create did not name a machine.") }
try Task.checkCancellation()
try dependencies.prepare(invocation.workspaceID)
dependencies.bind(invocation.workspaceID, machineID)
try await dependencies.connect(machineID)
try Task.checkCancellation()
let remoteWorkspaceID = try await dependencies.terminal(machineID, invocation.workspaceID, invocation.focus)
if let remoteWorkspaceID, !remoteWorkspaceID.isEmpty {
SurfaceCatalog.shared.bindCloudWorkspace(
localWorkspaceID: invocation.workspaceID,
machine: .cloud(machineID),
remoteWorkspaceID: remoteWorkspaceID,
isBase: false,
generatedTitle: String(localized: "workspace.cloudVM.defaultTitle", defaultValue: "Cloud VM")
)
}
output += "workspace=\(invocation.workspaceID.uuidString)\n"
return CloudVMActionLauncher.Completion(terminationStatus: 0, output: output, workspaceId: invocation.workspaceID, machineId: machineID)
} catch is CancellationError {
return CloudVMActionLauncher.Completion(terminationStatus: 1, output: output, workspaceId: nil, machineId: machineID, wasCancelled: true)
} catch {
return CloudVMActionLauncher.Completion(
terminationStatus: 1,
output: output + "\(error)\n",
workspaceId: nil,
machineId: machineID
)
}
}

@discardableResult
static func start(
arguments: [String],
operationID: UUID,
onOutput: (@MainActor (String) -> Void)?,
onCompletion: ((CloudVMActionLauncher.Completion) -> Void)?,
onCancellationReady: ((CloudVMActionLauncher.CancellationHandle) -> Void)?
) -> Bool {
guard let invocation = parse(arguments: arguments), let client = VMClient.shared else { return false }
let registry = CmuxTuiSurfaceProviderRegistry.shared
let deps = dependencies(client: client, registry: registry)
let task = Task { @MainActor in
let completion = await run(invocation, operationID: operationID, dependencies: deps, onOutput: { onOutput?($0) })
onCompletion?(completion)
}
onCancellationReady?(CloudVMActionLauncher.CancellationHandle { task.cancel() })
return true
}
}
5 changes: 5 additions & 0 deletions Sources/Cloud/MachineCreateCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ final class MachineCreateCoordinator {
@ObservationIgnored private var launches: [UUID: CancellableLaunch] = [:]
@ObservationIgnored private var handles: [UUID: CloudVMActionLauncher.CancellationHandle] = [:]
@ObservationIgnored private var workspaceWaiters: [UUID: CheckedContinuation<UUID?, Never>] = [:]
/// The operation currently entering a launcher; in-process create uses this as its idempotency scope.
@ObservationIgnored private(set) var launchingOperationID: UUID?
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
@ObservationIgnored private let notifier: @MainActor (MachineCreateNotice) -> Void
@ObservationIgnored private let selectWorkspace: SelectWorkspace
@ObservationIgnored private let cancelCreatedMachine: @MainActor (String) -> Void
Expand Down Expand Up @@ -206,6 +208,8 @@ final class MachineCreateCoordinator {
if isRetry, !request.isBaseSetup, let machineID {
arguments = ["vm", "open", machineID] + (request.presentationWorkspaceID.map { ["--workspace", $0.uuidString] } ?? []) + ["--focus", "false"]
}
let previousLaunchingOperationID = launchingOperationID
launchingOperationID = attempt.operationID
let handle = launch(arguments, { [weak self] chunk in
guard let self else { return }
self.apply(self.lifecycle.receive(chunk, from: attempt))
Expand All @@ -218,6 +222,7 @@ final class MachineCreateCoordinator {
)
self.apply(self.lifecycle.finish(completion, from: attempt))
})
launchingOperationID = previousLaunchingOperationID
guard let handle else {
let failure = isRetry ? String(localized: "machines.new.error.launch", defaultValue: "cmux could not start the create command. Sign in and try again.") : nil
apply(lifecycle.refuse(attempt, retryFailure: failure))
Expand Down
Loading
Loading