Skip to content
Merged
9 changes: 6 additions & 3 deletions .github/workflows/cmux-tui-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,12 @@ jobs:
macos_runner: ${{ inputs.macos_runner || vars.MACOS_RUNNER_BACKGROUND || 'macos-15' }}

publish:
# Production secrets: GitHub releases them only to protected refs (the
# environment's deployment branch policy: main, tags v*).
environment: release
# R2 upload credentials only. A commit-addressed cmux-tui build is not a
# production release: cmux-next pins daemon builds from helper branches.
# The artifacts environment's deployment branch policy allows main,
# feat-cmux-next and cmux-tui-pin-*; signing, Sparkle, Homebrew and Apple
# secrets stay in the release environment.
environment: artifacts
name: publish to R2
# PR runs of this workflow only validate the build; publishing is
# main-push or manual dispatch.
Expand Down
4 changes: 2 additions & 2 deletions CLI/CMUXCLI+AutoNaming.swift
Original file line number Diff line number Diff line change
Expand Up @@ -232,14 +232,14 @@ struct CodexAutoNamingArguments: Sendable {
arguments.insert("--ignore-user-config", at: arguments.firstIndex(of: "--ignore-rules")!)
}
guard let configToml else { return arguments }
let overrides = providerOverrides(from: configToml)
let overrides = providerOverrides(from: configToml, usesTemporaryConfig: usesTemporaryConfig)
for override in overrides.reversed() {
arguments.insert(contentsOf: ["-c", override], at: 1)
}
return arguments
}

private static func providerOverrides(from toml: String) -> [String] {
private static func providerOverrides(from toml: String, usesTemporaryConfig: Bool) -> [String] {
var model: String?
var modelProvider: String?
var providerEntries: [(section: String, key: String, value: String)] = []
Expand Down
2 changes: 1 addition & 1 deletion CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40474,7 +40474,7 @@ export default {
private static let openCodePluginFileName = "cmux-feed.js"

private func openCodeConfigDirPath() -> String {
OpenCodePaths.configDirectory(environment: ProcessInfo.processInfo.environment).path
OpenCodePaths(environment: ProcessInfo.processInfo.environment).configDirectory.path
}

private func openCodePluginPath(projectLocal: Bool) -> String {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Foundation

/// The filesystem locations used by OpenCode, resolved from its documented
/// environment overrides.
public struct OpenCodePaths: Sendable, Equatable {
/// OpenCode's configuration directory: `OPENCODE_CONFIG_DIR`, then
/// `$XDG_CONFIG_HOME/opencode`, then `~/.config/opencode`.
public let configDirectory: URL
/// OpenCode's session database: `OPENCODE_DB`, then
/// `$XDG_DATA_HOME/opencode/opencode.db`, then
/// `~/.local/share/opencode/opencode.db`.
public let databaseURL: URL

public init(environment: [String: String]) {
let home = Self.homeURL(environment: environment)
if let override = Self.nonEmpty(environment["OPENCODE_CONFIG_DIR"]) {
configDirectory = Self.expandedURL(override, home: home)
} else if let xdgConfigHome = Self.nonEmpty(environment["XDG_CONFIG_HOME"]) {
configDirectory = Self.expandedURL(xdgConfigHome, home: home)
.appendingPathComponent("opencode", isDirectory: true)
} else {
configDirectory = home.appendingPathComponent(".config/opencode", isDirectory: true)
}
if let override = Self.nonEmpty(environment["OPENCODE_DB"]) {
databaseURL = Self.expandedURL(override, home: home)
} else if let xdgDataHome = Self.nonEmpty(environment["XDG_DATA_HOME"]) {
databaseURL = Self.expandedURL(xdgDataHome, home: home)
.appendingPathComponent("opencode/opencode.db", isDirectory: false)
} else {
databaseURL = home.appendingPathComponent(".local/share/opencode/opencode.db", isDirectory: false)
}
}

private static func nonEmpty(_ value: String?) -> String? {
guard let value else { return nil }
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}

private static func expandedURL(_ path: String, home: URL) -> URL {
if path == "~" { return home }
if path.hasPrefix("~/") {
return home.appendingPathComponent(String(path.dropFirst(2)), isDirectory: false)
}
return URL(fileURLWithPath: NSString(string: path).expandingTildeInPath)
}

private static func homeURL(environment: [String: String]) -> URL {
if let home = nonEmpty(environment["HOME"]) {
return URL(fileURLWithPath: NSString(string: home).expandingTildeInPath)
}
return FileManager.default.homeDirectoryForCurrentUser
}
}
51 changes: 1 addition & 50 deletions Sources/SessionIndexModels.swift
Original file line number Diff line number Diff line change
@@ -1,55 +1,6 @@
import CMUXAgentLaunch
import Foundation

/// Resolves the filesystem locations used by OpenCode from its documented
/// environment overrides.
enum OpenCodePaths {
static func configDirectory(environment: [String: String]) -> URL {
let home = homeURL(environment: environment)
if let override = nonEmpty(environment["OPENCODE_CONFIG_DIR"]) {
return expandedURL(override, home: home)
}
if let xdgConfigHome = nonEmpty(environment["XDG_CONFIG_HOME"]) {
return expandedURL(xdgConfigHome, home: home)
.appendingPathComponent("opencode", isDirectory: true)
}
return home.appendingPathComponent(".config/opencode", isDirectory: true)
}

static func databaseURL(environment: [String: String]) -> URL {
let home = homeURL(environment: environment)
if let override = nonEmpty(environment["OPENCODE_DB"]) {
return expandedURL(override, home: home)
}
if let xdgDataHome = nonEmpty(environment["XDG_DATA_HOME"]) {
return expandedURL(xdgDataHome, home: home)
.appendingPathComponent("opencode/opencode.db", isDirectory: false)
}
return home.appendingPathComponent(".local/share/opencode/opencode.db", isDirectory: false)
}

private static func nonEmpty(_ value: String?) -> String? {
guard let value else { return nil }
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}

private static func expandedURL(_ path: String, home: URL) -> URL {
if path == "~" { return home }
if path.hasPrefix("~/") {
return home.appendingPathComponent(String(path.dropFirst(2)), isDirectory: false)
}
return URL(fileURLWithPath: NSString(string: path).expandingTildeInPath)
}

private static func homeURL(environment: [String: String]) -> URL {
if let home = nonEmpty(environment["HOME"]) {
return URL(fileURLWithPath: NSString(string: home).expandingTildeInPath)
}
return FileManager.default.homeDirectoryForCurrentUser
}
}

// MARK: - Agents

struct RegisteredSessionAgent: Hashable, Sendable {
Expand Down Expand Up @@ -200,7 +151,7 @@ enum OpenCodeDatabaseSnapshot {

static func make(prefix: String) throws -> Snapshot? {
let fileManager = FileManager.default
let sourceURL = OpenCodePaths.databaseURL(environment: ProcessInfo.processInfo.environment)
let sourceURL = OpenCodePaths(environment: ProcessInfo.processInfo.environment).databaseURL
guard fileManager.fileExists(atPath: sourceURL.path) else { return nil }

let snapshotDir = fileManager.temporaryDirectory.appendingPathComponent(
Expand Down
63 changes: 63 additions & 0 deletions Sources/Surfaces/CloudWorkspaceProjectionCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ final class CloudWorkspaceProjectionCoordinator {
guard let self else { return }
defer { if self.tasks[machine]?.id == id { self.tasks[machine] = nil } }
guard let catalog else { return }
var budget = CloudWorkspaceReconcileBudget()
while self.requested.remove(machine) != nil {
guard !Task.isCancelled, self.localMutations[machine] == nil else { return }
await catalog.cloudPlacementCoordinator.waitForPendingMutations()
Expand All @@ -51,12 +52,44 @@ final class CloudWorkspaceProjectionCoordinator {
let state = catalog.cloudStates[machine],
catalog.cloudStateObservations[machine]?.freshness == .current,
catalog.cloudPlacementCoordinator.allowsNativeReconciliation(state) else { return }
let progress = CloudWorkspaceReconcileBudget.Mark(
state: state,
projectionVersion: catalog.projectionVersions[machine, default: 0],
bindings: self.environment.bindings().filter { $0.value.vmID == machine.rawValue }
)
guard budget.admit(progress) else {
self.reportNonConvergence(machine: machine, state: state, budget: budget)
return
}
await self.reconcile(state: state, catalog: catalog)
}
}
tasks[machine] = CloudWorkspaceProjectionTask(id: id, task: task)
}

/// Daemon generation last reported per machine. A persistent re-requester
/// would otherwise report once for every graph revision.
private var reportedNonConvergence: [SurfaceMachineID: String] = [:]

/// Reports non-convergence once per daemon generation, so a trigger outside
/// this loop that keeps restarting it cannot flood crash reporting.
private func reportNonConvergence(machine: SurfaceMachineID, state: CloudVMState, budget: CloudWorkspaceReconcileBudget) {
#if DEBUG
cmuxDebugLog("cloudWorkspace.projection.nonConvergent machine=\(machine.rawValue) passes=\(budget.passes) idle=\(budget.idlePasses)")
#endif
let generation = state.cursor?.generation ?? ""
guard reportedNonConvergence[machine] != generation else { return }
reportedNonConvergence[machine] = generation
sentryCaptureWarning(
"Cloud workspace projection did not converge",
category: "cloud.projection",
data: [
"passes": budget.passes, "idlePasses": budget.idlePasses,
"generation": generation, "revision": state.cursor?.revision ?? 0,
]
)
}

/// A bound mirror may not recreate a view that the accepted graph removed.
/// Unbound viewers retain their existing attachment-repair behavior.
func retainsProjection(_ projection: SurfaceProjection, in state: CloudVMState) -> Bool {
Expand Down Expand Up @@ -96,6 +129,7 @@ final class CloudWorkspaceProjectionCoordinator {
tasks.removeValue(forKey: machine)?.task.cancel()
requested.remove(machine)
localMutations[machine] = nil
reportedNonConvergence[machine] = nil
let bindings = environment.bindings()
failures = failures.filter { bindings[$0.key]?.vmID != machine.rawValue }
}
Expand Down Expand Up @@ -170,3 +204,32 @@ final class CloudWorkspaceProjectionCoordinator {
failures = failures.filter { live.contains($0.key) }
}
}

/// Bounds reconciliation of one accepted graph. A pass that changes nothing
/// (same graph, projections and bindings as the pass before) cannot make the
/// next one different, so a few in a row mean a consumer is requesting passes
/// without progress. The hard ceiling also stops a loop that rewrites the same
/// projections every pass, which looks like progress.
struct CloudWorkspaceReconcileBudget {
struct Mark: Equatable {
let state: CloudVMState
let projectionVersion: UInt64
let bindings: [UUID: WorkspaceCloudVMBinding]
}

static let maxIdlePasses = 3
static let maxPassesPerState = 64

private var last: Mark?
private(set) var passes = 0
private(set) var idlePasses = 0

/// Records the catalog before a pass; false means stop reconciling this graph.
mutating func admit(_ mark: Mark) -> Bool {
if last?.state != mark.state { passes = 0; idlePasses = 0 }
idlePasses = last == mark ? idlePasses + 1 : 0
passes += 1
last = mark
return idlePasses < Self.maxIdlePasses && passes <= Self.maxPassesPerState
}
}
36 changes: 19 additions & 17 deletions Sources/Surfaces/SurfaceCatalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1245,16 +1245,21 @@ final class SurfaceCatalog {
}

func setRemotePlacement(for source: SurfaceProjection, workspaceID: String?, tabID: String?) {
// Only views whose coordinates change; an unchanged placement notifies nobody.
let matching = projections.filter {
$0.resource == source.resource && ($0.panelID == source.panelID
|| (tabID != nil && $0.remoteTabID == tabID))
&& ($0.remoteWorkspaceID != workspaceID || $0.remoteTabID != tabID)
}
guard !matching.isEmpty else { return }
var next = projections
for var projection in matching {
projections.remove(projection)
next.remove(projection)
projection.remoteWorkspaceID = workspaceID
projection.remoteTabID = tabID
projections.insert(projection)
next.insert(projection)
}
projections = next
notifyChange(for: source.resource.machine)
}

Expand All @@ -1264,23 +1269,20 @@ final class SurfaceCatalog {
@discardableResult
private func attachRemoteView(_ view: SurfaceRemoteView?, to projection: SurfaceProjection) -> SurfaceProjection {
guard let view else { return projection }
if view.isCloudDisplayMembershipView {
guard projection.remoteTabID == nil else { return projection }
projections.remove(projection)
var updated = projection
updated.remoteWorkspaceID = view.workspace.id
updated.remoteTabID = nil
projections.insert(updated)
reconcileCloudWorkspaceBinding(localWorkspaceID: updated.workspaceID)
notifyChange(for: updated.resource.machine)
return updated
}
guard projection.remoteTabID == nil || projection.remoteTabID == view.tabID else { return projection }
projections.remove(projection)
// A display membership view attaches only to a tabless preview.
let tabID = view.isCloudDisplayMembershipView ? nil : view.tabID
guard projection.remoteTabID == nil || projection.remoteTabID == tabID else { return projection }
var updated = projection
updated.remoteWorkspaceID = view.workspace.id
updated.remoteTabID = view.tabID
projections.insert(updated)
updated.remoteTabID = tabID
// Reconcile reprojects through here. Rewriting unchanged coordinates
// would request the next reconcile of this machine, forever.
guard updated != projection else { return projection }
// One assignment, so observers never see the pane briefly unprojected.
var next = projections
next.remove(projection)
next.insert(updated)
projections = next
reconcileCloudWorkspaceBinding(localWorkspaceID: updated.workspaceID)
notifyChange(for: updated.resource.machine)
return updated
Expand Down
5 changes: 4 additions & 1 deletion cmuxCLITests/CodexAutoNamingArgumentsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ struct CodexAutoNamingArgumentsTests {
let overrides = configOverrides(args)
#expect(overrides.contains("model_provider=\"subrouter\""))
#expect(overrides.contains("model=\"gpt-5-codex\""))
#expect(overrides.contains("model_providers.subrouter.base_url=\"http://127.0.0.1:31415/v1\""))
// With a temporary CODEX_HOME, the provider definition is already
// available in its mode-restricted config.toml. Do not duplicate it
// on argv, where nested provider values could expose credentials.
#expect(!overrides.contains("model_providers.subrouter.base_url=\"http://127.0.0.1:31415/v1\""))
#expect(!overrides.contains(where: {
$0.contains("secret") || $0.contains("experimental_bearer_token") || $0.contains("api-secret")
}))
Expand Down
Loading
Loading