Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 5 additions & 5 deletions .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# cmux-owned Swift file length budget.
# Format: max_lines<TAB>relative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
34475 CLI/cmux.swift
17604 Sources/AppDelegate.swift
34483 CLI/cmux.swift
17602 Sources/AppDelegate.swift
15933 Sources/ContentView.swift
14104 Sources/TerminalController.swift
12658 Sources/Workspace.swift
Expand All @@ -13,7 +13,7 @@
7953 Sources/Panels/BrowserPanelView.swift
7356 cmuxTests/WorkspaceUnitTests.swift
7221 cmuxTests/WorkspaceRemoteConnectionTests.swift
6317 cmuxTests/SessionPersistenceTests.swift
6579 cmuxTests/SessionPersistenceTests.swift
6223 cmuxTests/GhosttyConfigTests.swift
6172 Sources/TabManager.swift
6153 CLI/cmux_open.swift
Expand Down Expand Up @@ -46,10 +46,10 @@
2236 Sources/TerminalNotificationStore.swift
2117 cmuxTests/CmuxConfigTests.swift
2092 cmuxTests/ShortcutAndCommandPaletteTests.swift
2082 Sources/SessionPersistence.swift
2068 Sources/SessionPersistence.swift
1949 Sources/Panels/BrowserWebAuthnSupport.swift
1941 Sources/KeyboardShortcutSettingsFileStore.swift
1880 Sources/RestorableAgentSession.swift
1900 Sources/RestorableAgentSession.swift
1860 cmuxTests/NotificationAndMenuBarTests.swift
1810 Sources/SessionIndexStore.swift
1748 Sources/WindowDragHandleView.swift
Expand Down
18 changes: 13 additions & 5 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20547,16 +20547,24 @@ struct CMUXCLI {

private static func codexTeamsStartupScript(commandText: String, cwd: String?) -> String? {
let scriptURL = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-codex-teams-\(UUID().uuidString.lowercased()).sh")
.appendingPathComponent("cmux-codex-teams-\(UUID().uuidString.lowercased()).zsh")
var lines = [
"#!/bin/sh",
"#!/bin/zsh",
"rm -f -- \"$0\" 2>/dev/null || true"
]
if let cwd = cwd?.trimmingCharacters(in: .whitespacesAndNewlines), !cwd.isEmpty {
let trimmedCwd = cwd?.trimmingCharacters(in: .whitespacesAndNewlines)
let launchCommand: String
if let cwd = trimmedCwd, !cwd.isEmpty {
let quotedCwd = codexTeamsShellQuote(cwd)
lines.append("{ cd -- \(quotedCwd) 2>/dev/null || [ ! -d \(quotedCwd) ]; } || exit $?")
launchCommand = "{ cd -- \(quotedCwd) 2>/dev/null || [ ! -d \(quotedCwd) ]; } && \(commandText)"
} else {
launchCommand = commandText
}
lines.append("exec \"${SHELL:-/bin/sh}\" -lc \(codexTeamsShellQuote(commandText))")
lines.append(contentsOf: AgentResumeShellScriptBuilder().commandThenReturnLines(
command: launchCommand,
workingDirectory: trimmedCwd,
retryPolicy: .codexStateDatabaseLock
))
do {
try (lines.joined(separator: "\n") + "\n").write(
to: scriptURL,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import Foundation

/// Describes when an agent resume command should be retried after a transient startup failure.
///
/// The policy is intentionally narrow: callers opt in for known retryable agents, and shell
/// launchers only retry fast failures whose bounded startup capture contains one of
/// ``outputNeedles``. Callers that already have failed process output can use ``matches(output:)``
/// to check the same known retryable signatures.
public struct AgentResumeRetryPolicy: Sendable, Equatable {
/// The maximum number of retries after the first failed launch attempt.
public let maximumRetries: Int

/// The delay between retry attempts, before the per-process stagger is added.
public let delaySeconds: Double

/// The startup window, in whole seconds, during which a failed shell launch may be retried.
public let startupFailureWindowSeconds: Int

/// Case-insensitive output fragments that identify a retryable transient failure.
public let outputNeedles: [String]

/// Creates a retry policy for a generated resume launcher.
///
/// - Parameters:
/// - maximumRetries: The maximum number of retries after the first failed launch attempt.
/// - delaySeconds: The delay between retry attempts.
/// - outputNeedles: Case-insensitive output fragments that identify retryable failures.
/// - startupFailureWindowSeconds: The shell-launch startup window that allows retrying a
/// failed attempt without recording an interactive transcript.
public init(
maximumRetries: Int,
delaySeconds: Double,
outputNeedles: [String],
startupFailureWindowSeconds: Int = 5
) {
self.maximumRetries = max(0, maximumRetries)
self.delaySeconds = max(0, delaySeconds)
self.startupFailureWindowSeconds = max(0, startupFailureWindowSeconds)
self.outputNeedles = outputNeedles
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
}

/// A policy that never retries.
public static let disabled = AgentResumeRetryPolicy(
maximumRetries: 0,
delaySeconds: 0,
outputNeedles: [],
startupFailureWindowSeconds: 0
)

/// Retries Codex's transient shared-state SQLite lock failure.
public static let codexStateDatabaseLock = AgentResumeRetryPolicy(
maximumRetries: 3,
delaySeconds: 0.25,
outputNeedles: [
"database is locked",
"another Codex process is using its local data",
],
startupFailureWindowSeconds: 5
)

/// Returns `true` when this policy can retry at least one failure.
public var isEnabled: Bool {
maximumRetries > 0 && !outputNeedles.isEmpty
}

/// Chooses the retry policy for a captured agent kind and launcher.
///
/// - Parameters:
/// - agentKind: The raw agent kind, such as `"codex"`.
/// - launcher: The captured launcher name, such as `"codexTeams"`.
/// - Returns: ``codexStateDatabaseLock`` for Codex launches; otherwise ``disabled``.
public static func policy(agentKind: String?, launcher: String? = nil) -> AgentResumeRetryPolicy {
if normalized(agentKind) == "codex" || normalized(launcher) == "codexteams" {
return .codexStateDatabaseLock
}
return .disabled
}

/// Returns `true` when process output contains a retryable signature.
///
/// - Parameter output: Combined stdout/stderr from the failed attempt.
/// - Returns: `true` when `output` contains a retryable signature; otherwise `false`.
public func matches(output: String) -> Bool {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
guard isEnabled else { return false }
let lowercasedOutput = output.lowercased()
return outputNeedles.contains { lowercasedOutput.contains($0.lowercased()) }
}

var shellGrepPattern: String {
outputNeedles
.filter { !$0.isEmpty }
.map(Self.extendedGrepEscaped)
.joined(separator: "|")
}

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

private static func extendedGrepEscaped(_ value: String) -> String {
let specialCharacters = #"[]\.^$*+?{}()|"#
var result = ""
for character in value {
if specialCharacters.contains(character) {
result.append("\\")
}
result.append(character)
}
return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation

struct AgentResumeShellQuoting: Sendable, Equatable {
func singleQuoted(_ value: String) -> String {
if value.utf8.contains(where: { $0 >= 0x80 }) {
return asciiPrintfCommandSubstitution(for: value)
}
return "'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'"
}

private func asciiPrintfCommandSubstitution(for value: String) -> String {
let octalBytes = value.utf8
.map { String(format: #"\%03o"#, Int($0)) }
.joined()
return #""$(printf '"# + octalBytes + #"')""#
}
Comment thread
austinywang marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import Foundation

/// Builds zsh launcher-script lines for an agent resume command.
///
/// The builder owns the shell shape shared by local app restores and CLI-created resume surfaces:
/// run the supplied command in a child shell, optionally retry a known transient failure, then exec
/// a fresh login shell from the saved working directory so a failed or exited agent leaves the
/// visible terminal where the session lived.
public struct AgentResumeShellScriptBuilder: Sendable, Equatable {
private let quoting = AgentResumeShellQuoting()

/// Creates a shell-script builder. The type holds no runtime state.
public init() {}

/// Builds the launcher lines that run `command` and then return to a login shell.
///
/// The command is executed exactly as supplied. If the command must start from a directory, the
/// caller should include that `cd` in the command text. The `workingDirectory` value controls the
/// directory restored before the final login shell is `exec`ed.
///
/// - Parameters:
/// - command: The shell command to run in the child shell.
/// - workingDirectory: The directory for the final visible shell after the command exits.
/// - retryPolicy: A bounded retry policy for transient launch failures.
/// - Returns: zsh script lines to append after a shebang.
public func commandThenReturnLines(
command: String,
workingDirectory: String? = nil,
retryPolicy: AgentResumeRetryPolicy = .disabled
) -> [String] {
var lines = [
#"_cmux_resume_shell="${SHELL:-/bin/zsh}""#,
]
if retryPolicy.isEnabled {
lines.append(contentsOf: retryingCommandLines(command: command, retryPolicy: retryPolicy))
} else {
lines.append(contentsOf: plainCommandLines(command: command))
}
lines.append(contentsOf: zshIntegrationReentryLines)
if let workingDirectory,
!workingDirectory.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
lines.append(#"{ cd -- \#(quoting.singleQuoted(workingDirectory)) 2>/dev/null || true; }"#)
}
lines.append(#"exec -l "$_cmux_resume_shell""#)
return lines
}

private var zshIntegrationReentryLines: [String] {
[
#"if [[ "${_cmux_resume_shell:t}" == "zsh" && -n "${CMUX_SHELL_INTEGRATION_DIR:-}" && -r "${CMUX_SHELL_INTEGRATION_DIR}/.zshenv" ]]; then"#,
#" if [[ -n "${ZDOTDIR+X}" ]]; then"#,
#" export CMUX_ZSH_ZDOTDIR="$ZDOTDIR""#,
#" else"#,
#" unset CMUX_ZSH_ZDOTDIR"#,
#" fi"#,
#" export ZDOTDIR="$CMUX_SHELL_INTEGRATION_DIR""#,
#"fi"#,
]
}

private func plainCommandLines(command: String) -> [String] {
let quotedCommand = quoting.singleQuoted(command)
return [
#"case "${_cmux_resume_shell:t}" in"#,
#" zsh|bash) "$_cmux_resume_shell" -lic \#(quotedCommand) ;;"#,
#" csh|tcsh) "$_cmux_resume_shell" -c \#(quotedCommand) ;;"#,
#" *) "$_cmux_resume_shell" -c \#(quotedCommand) ;;"#,
#"esac"#,
]
}

private func retryingCommandLines(command: String, retryPolicy: AgentResumeRetryPolicy) -> [String] {
let quotedCommand = quoting.singleQuoted(command)
let quotedPattern = quoting.singleQuoted(retryPolicy.shellGrepPattern)
let retryCount = max(0, retryPolicy.maximumRetries)
let retryDelay = String(format: "%.3f", max(0, retryPolicy.delaySeconds))
let retryStartupSeconds = max(0, retryPolicy.startupFailureWindowSeconds)
return [
#"_cmux_resume_command=\#(quotedCommand)"#,
#"_cmux_resume_retry_limit="${CMUX_AGENT_RESUME_RETRY_LIMIT:-\#(retryCount)}""#,
#"case "$_cmux_resume_retry_limit" in"#,
#" ''|*[!0-9]*) _cmux_resume_retry_limit=\#(retryCount) ;;"#,
#"esac"#,
#"_cmux_resume_retry_delay="${CMUX_AGENT_RESUME_RETRY_DELAY_SECONDS:-\#(retryDelay)}""#,
#"case "$_cmux_resume_retry_delay" in"#,
#" ''|*[!0-9.]*) _cmux_resume_retry_delay=\#(retryDelay) ;;"#,
#"esac"#,
#"_cmux_resume_retry_startup_seconds="${CMUX_AGENT_RESUME_RETRY_STARTUP_SECONDS:-\#(retryStartupSeconds)}""#,
#"case "$_cmux_resume_retry_startup_seconds" in"#,
#" ''|*[!0-9]*) _cmux_resume_retry_startup_seconds=\#(retryStartupSeconds) ;;"#,
#"esac"#,
#"_cmux_resume_retry=0"#,
#"_cmux_resume_log="""#,
#"_cmux_resume_pipe="""#,
#"_cmux_resume_cleanup_log() {"#,
#" if [ -n "$_cmux_resume_log" ]; then"#,
#" rm -f -- "$_cmux_resume_log" 2>/dev/null || true"#,
#" fi"#,
#" if [ -n "$_cmux_resume_pipe" ]; then"#,
#" rm -f -- "$_cmux_resume_pipe" 2>/dev/null || true"#,
#" fi"#,
#"}"#,
#"_cmux_resume_capture_log() {"#,
#" /bin/dd bs=1 count=4096 of="$1" 2>/dev/null"#,
#" /bin/cat >/dev/null"#,
#"}"#,
#"trap _cmux_resume_cleanup_log EXIT INT TERM"#,
#"while true; do"#,
#" _cmux_resume_log="${TMPDIR:-/tmp}/cmux-agent-resume-${$}-${_cmux_resume_retry}.log""#,
#" _cmux_resume_pipe="${TMPDIR:-/tmp}/cmux-agent-resume-${$}-${_cmux_resume_retry}.fifo""#,
#" rm -f -- "$_cmux_resume_log" "$_cmux_resume_pipe" 2>/dev/null || true"#,
#" : > "$_cmux_resume_log" 2>/dev/null && chmod 600 "$_cmux_resume_log" 2>/dev/null || true"#,
#" _cmux_resume_script_output="""#,
#" _cmux_resume_capture_pid="""#,
#" if [ -x /usr/bin/script ] && [ -x /usr/bin/mkfifo ] && /usr/bin/mkfifo "$_cmux_resume_pipe" 2>/dev/null; then"#,
#" _cmux_resume_capture_log "$_cmux_resume_log" < "$_cmux_resume_pipe" &"#,
#" _cmux_resume_capture_pid=$!"#,
#" _cmux_resume_script_output="$_cmux_resume_pipe""#,
#" fi"#,
#" _cmux_resume_started_at=$(/bin/date +%s 2>/dev/null || echo 0)"#,
#" case "${_cmux_resume_shell:t}" in"#,
#" zsh|bash)"#,
#" if [ -n "$_cmux_resume_capture_pid" ] && [ -x /usr/bin/script ]; then"#,
#" /usr/bin/script -q -F "$_cmux_resume_script_output" "$_cmux_resume_shell" -lic "$_cmux_resume_command""#,
#" else"#,
#" "$_cmux_resume_shell" -lic "$_cmux_resume_command""#,
#" fi"#,
#" ;;"#,
#" csh|tcsh)"#,
#" if [ -n "$_cmux_resume_capture_pid" ] && [ -x /usr/bin/script ]; then"#,
#" /usr/bin/script -q -F "$_cmux_resume_script_output" "$_cmux_resume_shell" -c "$_cmux_resume_command""#,
#" else"#,
#" "$_cmux_resume_shell" -c "$_cmux_resume_command""#,
#" fi"#,
#" ;;"#,
#" *)"#,
#" if [ -n "$_cmux_resume_capture_pid" ] && [ -x /usr/bin/script ]; then"#,
#" /usr/bin/script -q -F "$_cmux_resume_script_output" "$_cmux_resume_shell" -c "$_cmux_resume_command""#,
#" else"#,
#" "$_cmux_resume_shell" -c "$_cmux_resume_command""#,
#" fi"#,
#" ;;"#,
#" esac"#,
#" _cmux_resume_status=$?"#,
#" if [ -n "$_cmux_resume_capture_pid" ]; then"#,
#" wait "$_cmux_resume_capture_pid" 2>/dev/null || true"#,
#" fi"#,
#" rm -f -- "$_cmux_resume_pipe" 2>/dev/null || true"#,
#" if [ "$_cmux_resume_status" -eq 0 ]; then"#,
#" break"#,
#" fi"#,
#" _cmux_resume_finished_at=$(/bin/date +%s 2>/dev/null || echo 0)"#,
#" _cmux_resume_elapsed=$((_cmux_resume_finished_at - _cmux_resume_started_at))"#,
#" if [ "$_cmux_resume_elapsed" -lt 0 ]; then"#,
#" _cmux_resume_elapsed=$_cmux_resume_retry_startup_seconds"#,
#" fi"#,
#" if [ "$_cmux_resume_elapsed" -gt "$_cmux_resume_retry_startup_seconds" ]; then"#,
#" break"#,
#" fi"#,
#" if ! /usr/bin/grep -Eiq \#(quotedPattern) "$_cmux_resume_log" 2>/dev/null; then"#,
#" break"#,
#" fi"#,
#" if [ "$_cmux_resume_retry" -ge "$_cmux_resume_retry_limit" ]; then"#,
#" break"#,
#" fi"#,
#" _cmux_resume_cleanup_log"#,
#" _cmux_resume_retry=$((_cmux_resume_retry + 1))"#,
#" if [ "$_cmux_resume_retry_delay" != "0" ]; then"#,
#" sleep "$_cmux_resume_retry_delay""#,
#" _cmux_resume_stagger=$((($$ + _cmux_resume_retry) % 7))"#,
#" if [ "$_cmux_resume_stagger" -gt 0 ]; then"#,
#" sleep "0.${_cmux_resume_stagger}""#,
#" fi"#,
#" fi"#,
#"done"#,
#"_cmux_resume_cleanup_log"#,
#"trap - EXIT INT TERM"#,
]
}
}
Loading
Loading