diff --git a/CLI/cmux.swift b/CLI/cmux.swift index 0b352555bb09..8b6bd3f6f07c 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -24881,16 +24881,14 @@ struct CMUXCLI { client: client, workspaceId: workspaceId, surfaceId: cleanupSurfaceId, - sessionId: consumedSession.sessionId, - sessionDidEnd: true + sessionId: consumedSession.sessionId ) if cleanupSurfaceId != consumedSession.surfaceId { clearAgentSurfaceResumeBinding( client: client, workspaceId: consumedSession.workspaceId, surfaceId: consumedSession.surfaceId, - sessionId: consumedSession.sessionId, - sessionDidEnd: true + sessionId: consumedSession.sessionId ) } sendClaudeFeedTelemetry(workspaceId: workspaceId, surfaceId: cleanupSurfaceId) @@ -28022,8 +28020,7 @@ struct CMUXCLI { client: SocketClient, workspaceId: String, surfaceId: String, - sessionId: String?, - sessionDidEnd: Bool = false + sessionId: String? ) { let normalizedSessionId = normalizedHookValue(sessionId) var params: [String: Any] = [ @@ -28033,9 +28030,6 @@ struct CMUXCLI { if let normalizedSessionId { params["checkpoint_id"] = normalizedSessionId } - if sessionDidEnd, normalizedSessionId != nil { - params["agent_session_ended"] = true - } _ = try? client.sendV2(method: "surface.resume.clear", params: params) } @@ -30551,8 +30545,7 @@ export default CMUXSessionRestore; client: client, workspaceId: consumed.workspaceId, surfaceId: consumed.surfaceId, - sessionId: consumed.sessionId, - sessionDidEnd: true + sessionId: consumed.sessionId ) _ = try? sendV1Command( "clear_agent_pid \(pidKey) --tab=\(consumed.workspaceId)\(socketPanelOption(consumed.surfaceId)) --clear-status", diff --git a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface3.swift b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface3.swift index 8bbfed6f360b..89bb46ae7399 100644 --- a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface3.swift +++ b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface3.swift @@ -113,38 +113,17 @@ extension ControlCommandCoordinator { guard context?.controlSurfaceRoutingResolvesTabManager(routing: routing) ?? false else { return .err(code: "unavailable", message: Self.surfaceWindowUnavailableMessage, data: nil) } - let agentSessionEnded: Bool - switch params["agent_session_ended"] { - case .none: - agentSessionEnded = false - case .some(.bool(let value)): - agentSessionEnded = value - case .some: - return .err( - code: "invalid_params", - message: surfaceResumeStrings().agentSessionEndedMustBeBoolean, - data: nil - ) - } let resolution = context?.controlSurfaceResumeClear( routing: routing, explicitTargetID: surfaceResumeExplicitTargetID(params), hasResolvedWindowID: uuid(params, "window_id") != nil, expectedCheckpointID: optionalTrimmedRawString(params, "checkpoint_id") ?? optionalTrimmedRawString(params, "checkpointId"), - expectedSource: optionalTrimmedRawString(params, "source"), - agentSessionEnded: agentSessionEnded + expectedSource: optionalTrimmedRawString(params, "source") ) ?? .surfaceNotFound return surfaceResumeResult(resolution) } - /// The localized surface-resume strings supplied by the app bundle. - private func surfaceResumeStrings() -> ControlSurfaceResumeStrings { - context?.controlSurfaceResumeStrings() ?? ControlSurfaceResumeStrings( - agentSessionEndedMustBeBoolean: "" - ) - } - /// Shapes the shared `surface.resume.*` result. private func surfaceResumeResult(_ resolution: ControlSurfaceResumeResolution) -> ControlCallResult { switch resolution { diff --git a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceContext.swift b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceContext.swift index 8f7938b57f10..cd0dd75a7d5f 100644 --- a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceContext.swift +++ b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceContext.swift @@ -228,12 +228,6 @@ public protocol ControlSurfaceContext: AnyObject { // MARK: - resume.set / get / clear - /// The app-bundle-resolved localized validation strings for - /// `surface.resume.*` commands. - /// - /// - Returns: The surface-resume strings. - func controlSurfaceResumeStrings() -> ControlSurfaceResumeStrings - /// Sets a resume binding for `surface.resume.set`. The app resolves the /// target, runs the (possibly blocking, app-bundle-localized) approval flow, /// and stores the binding. @@ -267,16 +261,13 @@ public protocol ControlSurfaceContext: AnyObject { /// - routing: The routing selectors (with the surface-resume precedence). /// - expectedCheckpointID: The optional expected checkpoint guard. /// - expectedSource: The optional expected source guard. - /// - agentSessionEnded: Whether a managed hook is clearing the binding as - /// part of authoritative session teardown. /// - Returns: The resume resolution. func controlSurfaceResumeClear( routing: ControlRoutingSelectors, explicitTargetID: UUID?, hasResolvedWindowID: Bool, expectedCheckpointID: String?, - expectedSource: String?, - agentSessionEnded: Bool + expectedSource: String? ) -> ControlSurfaceResumeResolution // MARK: - report_tty / report_pwd / report_shell_state / ports_kick diff --git a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceResumeStrings.swift b/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceResumeStrings.swift deleted file mode 100644 index a40786757485..000000000000 --- a/Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceResumeStrings.swift +++ /dev/null @@ -1,16 +0,0 @@ -/// Localized validation messages for surface resume commands. -/// -/// The app supplies these values so `String(localized:)` resolves against the -/// app bundle instead of the package bundle. -public struct ControlSurfaceResumeStrings: Sendable, Equatable { - /// The message returned when `agent_session_ended` is not a JSON boolean. - public let agentSessionEndedMustBeBoolean: String - - /// Creates the localized surface-resume message bundle. - /// - /// - Parameter agentSessionEndedMustBeBoolean: The malformed - /// `agent_session_ended` message. - public init(agentSessionEndedMustBeBoolean: String) { - self.agentSessionEndedMustBeBoolean = agentSessionEndedMustBeBoolean - } -} diff --git a/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift index 09641d7a3642..5f2722b664f6 100644 --- a/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift +++ b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift @@ -445,10 +445,6 @@ extension ControlSurfaceContext { ControlSurfaceInputStrings(inputQueueFull: "", surfaceUnavailable: "", processExited: "") } - func controlSurfaceResumeStrings() -> ControlSurfaceResumeStrings { - ControlSurfaceResumeStrings(agentSessionEndedMustBeBoolean: "") - } - func controlSurfaceSendText( routing: ControlRoutingSelectors, surfaceID: UUID?, @@ -481,8 +477,7 @@ extension ControlSurfaceContext { explicitTargetID: UUID?, hasResolvedWindowID: Bool, expectedCheckpointID: String?, - expectedSource: String?, - agentSessionEnded: Bool + expectedSource: String? ) -> ControlSurfaceResumeResolution { .surfaceNotFound } nonisolated func controlSurfaceParseShellActivityState(_ rawState: String) -> String? { nil } diff --git a/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorSurfaceTests.swift b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorSurfaceTests.swift index 283b8fd21c96..87d5c3495bfe 100644 --- a/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorSurfaceTests.swift +++ b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorSurfaceTests.swift @@ -211,50 +211,6 @@ struct ControlCommandCoordinatorSurfaceTests { )) } - @Test func surfaceResumeClearForwardsManagedSessionEndProvenance() { - let context = FakeSurfaceControlCommandContext() - let coordinator = ControlCommandCoordinator(context: context) - - _ = coordinator.handle(ControlRequest( - id: .int(1), - method: "surface.resume.clear", - params: ["agent_session_ended": .bool(true)] - )) - #expect(context.resumeClearAgentSessionEnded == true) - - _ = coordinator.handle(ControlRequest( - id: .int(2), - method: "surface.resume.clear", - params: [:] - )) - #expect(context.resumeClearAgentSessionEnded == false) - } - - @Test( - "surface resume clear rejects malformed session-end provenance", - arguments: [JSONValue.null, .int(1), .string("true")] - ) - func surfaceResumeClearRejectsMalformedSessionEndProvenance(value: JSONValue) { - let context = FakeSurfaceControlCommandContext() - context.resumeStrings = ControlSurfaceResumeStrings( - agentSessionEndedMustBeBoolean: "localized boolean validation" - ) - let coordinator = ControlCommandCoordinator(context: context) - - let result = coordinator.handle(ControlRequest( - id: .int(1), - method: "surface.resume.clear", - params: ["agent_session_ended": value] - )) - - #expect(result == .err( - code: "invalid_params", - message: "localized boolean validation", - data: nil - )) - #expect(context.resumeClearAgentSessionEnded == nil) - } - @Test func paneCreateDockUnsupportedTypeReturnsInvalidParams() throws { let context = FakeSurfaceControlCommandContext() context.paneCreateResolution = .dockUnsupportedType( diff --git a/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/FakeSurfaceControlCommandContext.swift b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/FakeSurfaceControlCommandContext.swift index d3139051e033..f998efc13216 100644 --- a/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/FakeSurfaceControlCommandContext.swift +++ b/Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/FakeSurfaceControlCommandContext.swift @@ -7,10 +7,6 @@ final class FakeSurfaceControlCommandContext: ControlCommandContext { var createResolution: ControlSurfaceCreateResolution = .tabManagerUnavailable var surfaceListSnapshot: ControlSurfaceListSnapshot? var resumeResolution: ControlSurfaceResumeResolution = .surfaceNotFound - var resumeClearAgentSessionEnded: Bool? - var resumeStrings = ControlSurfaceResumeStrings( - agentSessionEndedMustBeBoolean: "agent_session_ended must be a boolean" - ) var reportPWDResolution: ControlSurfaceReportPWDResolution = .recorded(surfaceID: UUID()) var reportedPWD: (workspaceID: UUID, requestedSurfaceID: UUID?, path: String)? var reportGitResolution: ControlSurfaceReportGitBranchResolution = .recorded(surfaceID: UUID()) @@ -57,22 +53,6 @@ final class FakeSurfaceControlCommandContext: ControlCommandContext { resumeResolution } - func controlSurfaceResumeStrings() -> ControlSurfaceResumeStrings { - resumeStrings - } - - func controlSurfaceResumeClear( - routing: ControlRoutingSelectors, - explicitTargetID: UUID?, - hasResolvedWindowID: Bool, - expectedCheckpointID: String?, - expectedSource: String?, - agentSessionEnded: Bool - ) -> ControlSurfaceResumeResolution { - resumeClearAgentSessionEnded = agentSessionEnded - return resumeResolution - } - func controlSurfaceReportPWD( workspaceID: UUID, requestedSurfaceID: UUID?, diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/TerminalCatalogSection.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/TerminalCatalogSection.swift index 47f129ab76b4..bc9b0ba3e372 100644 --- a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/TerminalCatalogSection.swift +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/TerminalCatalogSection.swift @@ -49,13 +49,6 @@ public struct TerminalCatalogSection: SettingCatalogSection { userDefaultsKey: "terminal.autoResumeAgentSessions" ) - /// Whether failed active agent sessions automatically resume with bounded retries. - public let autoRetryAgentSessions = DefaultsKey( - id: "terminal.autoRetryAgentSessions", - defaultValue: false, - userDefaultsKey: "terminal.autoRetryAgentSessions" - ) - public let agentHibernationEnabled = DefaultsKey( id: "terminal.agentHibernation.enabled", defaultValue: false, diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Environment/SettingsHostActions.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Environment/SettingsHostActions.swift index c35d6e25178b..554bcac82fec 100644 --- a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Environment/SettingsHostActions.swift +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Environment/SettingsHostActions.swift @@ -187,9 +187,6 @@ public protocol SettingsHostActions: AnyObject { /// catalog-backed setting. func resetAllSettingsSideEffects() - /// Publishes the committed auto-retry setting to live workspace coordinators. - func agentSessionAutoRetrySettingDidChange() - /// Invalidates host-owned shortcut caches after Settings persists a shortcut change. func notifyShortcutSettingsDidChange() @@ -214,9 +211,6 @@ public extension SettingsHostActions { /// Default no-op for hosts with no app-owned reset side effects. func resetAllSettingsSideEffects() {} - /// Default no-op for previews and tests without agent retry coordination. - func agentSessionAutoRetrySettingDidChange() {} - /// Default no-op for hosts with no app-owned shortcut caches. func notifyShortcutSettingsDidChange() {} diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift index 680a27fb93f6..e21cac1e3b31 100644 --- a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift @@ -112,15 +112,6 @@ extension Array where Element == CuratedSettingEntry { ), .init(section: .terminal, id: "copy-on-select", title: "Copy on Selection", synonyms: "terminal.copyOnSelect copy on selection select clipboard mouse double click triple click iterm"), .init(section: .terminal, id: "agent-auto-resume", title: "Resume Agent Sessions on Reopen", synonyms: "terminal.autoResumeAgentSessions auto resume restore reopen relaunch quit sessions agents claude code codex opencode rovo dev rovodev toggle"), - .init( - section: .terminal, - id: "agent-auto-retry", - title: String(localized: "settings.terminal.agentAutoRetry", defaultValue: "Retry Failed Agent Sessions"), - synonyms: String( - localized: "settings.search.alias.setting.terminal.agent-auto-retry", - defaultValue: "terminal.autoRetryAgentSessions auto retry resume error failure transport api rate limit overloaded agents claude codex" - ) - ), .init(section: .terminal, id: "agent-hibernation", title: "Agent Hibernation", synonyms: "terminal.agentHibernation.enabled idle hibernate suspend background agents claude code codex opencode live terminals"), .init(section: .terminal, id: "agent-hibernation-idle", title: "Hibernate After Idle Seconds", synonyms: "terminal.agentHibernation.idleSeconds idle seconds timeout delay hibernate suspend"), .init(section: .terminal, id: "agent-hibernation-max", title: "Max Live Agent Terminals", synonyms: "terminal.agentHibernation.maxLiveTerminals max live agent terminals limit count hibernate"), diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AgentRecoverySettingsModel.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AgentRecoverySettingsModel.swift deleted file mode 100644 index c9ab5f36b1f0..000000000000 --- a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AgentRecoverySettingsModel.swift +++ /dev/null @@ -1,43 +0,0 @@ -import CmuxSettings -import Observation - -/// Settings state and committed host side effects for agent recovery controls. -@MainActor -@Observable -final class AgentRecoverySettingsModel { - let autoResume: DefaultsValueModel - let autoRetry: DefaultsValueModel - - @ObservationIgnored private let hostActions: any SettingsHostActions - - init( - defaultsStore: UserDefaultsSettingsStore, - catalog: SettingCatalog, - hostActions: any SettingsHostActions - ) { - autoResume = DefaultsValueModel( - store: defaultsStore, - key: catalog.terminal.autoResumeAgentSessions - ) - autoRetry = DefaultsValueModel( - store: defaultsStore, - key: catalog.terminal.autoRetryAgentSessions - ) - self.hostActions = hostActions - } - - func startObserving() { - autoResume.startObserving() - autoRetry.startObserving() - } - - func setAutoResume(_ enabled: Bool) { - autoResume.set(enabled) - } - - func setAutoRetry(_ enabled: Bool) { - autoRetry.set(enabled) { [hostActions] in - hostActions.agentSessionAutoRetrySettingDidChange() - } - } -} diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AgentRecoverySettingsRows.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AgentRecoverySettingsRows.swift deleted file mode 100644 index d6a0b3efc7ce..000000000000 --- a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AgentRecoverySettingsRows.swift +++ /dev/null @@ -1,74 +0,0 @@ -import CmuxSettings -import SwiftUI - -@MainActor -struct AgentRecoverySettingsRows: View { - @State private var model: AgentRecoverySettingsModel - - init( - defaultsStore: UserDefaultsSettingsStore, - catalog: SettingCatalog, - hostActions: any SettingsHostActions - ) { - _model = State(initialValue: AgentRecoverySettingsModel( - defaultsStore: defaultsStore, - catalog: catalog, - hostActions: hostActions - )) - } - - var body: some View { - SettingsCardRow( - configurationReview: .json("terminal.autoResumeAgentSessions"), - String( - localized: "settings.terminal.agentAutoResume", - defaultValue: "Resume Agent Sessions on Reopen" - ), - subtitle: model.autoResume.current - ? String( - localized: "settings.terminal.agentAutoResume.subtitleOn", - defaultValue: "When cmux reopens after quit, restored agent terminals automatically run their resume command." - ) - : String( - localized: "settings.terminal.agentAutoResume.subtitleOff", - defaultValue: "When cmux reopens after quit, restored agent terminals stay idle until you resume them manually." - ) - ) { - Toggle("", isOn: Binding( - get: { model.autoResume.current }, - set: { model.setAutoResume($0) } - )) - .labelsHidden() - .controlSize(.small) - .accessibilityIdentifier("SettingsTerminalAgentAutoResumeToggle") - } - SettingsCardDivider() - SettingsCardRow( - configurationReview: .json("terminal.autoRetryAgentSessions"), - String( - localized: "settings.terminal.agentAutoRetry", - defaultValue: "Retry Failed Agent Sessions" - ), - subtitle: model.autoRetry.current - ? String( - localized: "settings.terminal.agentAutoRetry.subtitleOn", - defaultValue: "Agent sessions that exit with an error resume automatically with bounded backoff." - ) - : String( - localized: "settings.terminal.agentAutoRetry.subtitleOff", - defaultValue: "Errored agent sessions stay stopped until you resume them manually." - ) - ) { - Toggle("", isOn: Binding( - get: { model.autoRetry.current }, - set: { model.setAutoRetry($0) } - )) - .labelsHidden() - .controlSize(.small) - .accessibilityIdentifier("SettingsTerminalAgentAutoRetryToggle") - } - .task { - model.startObserving() - } - } -} diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/TerminalSection.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/TerminalSection.swift index b1b61a625503..ee00342b6986 100644 --- a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/TerminalSection.swift +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/TerminalSection.swift @@ -8,7 +8,6 @@ import SwiftUI /// the JSON-backed Resume Commands editor. @MainActor public struct TerminalSection: View { - private let defaultsStore: UserDefaultsSettingsStore private let jsonStore: JSONConfigStore private let catalog: SettingCatalog private let hostActions: SettingsHostActions @@ -24,6 +23,7 @@ public struct TerminalSection: View { @State private var sessionContentAlignment: DefaultsValueModel @State private var scrollBar: DefaultsValueModel @State private var copyOnSelect: DefaultsValueModel + @State private var autoResume: DefaultsValueModel @State private var hibernation: DefaultsValueModel @State private var idleSeconds: DefaultsValueModel @State private var maxLive: DefaultsValueModel @@ -39,7 +39,6 @@ public struct TerminalSection: View { catalog: SettingCatalog, hostActions: SettingsHostActions ) { - self.defaultsStore = defaultsStore self.jsonStore = jsonStore self.catalog = catalog self.hostActions = hostActions @@ -50,6 +49,7 @@ public struct TerminalSection: View { _sessionContentAlignment = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.sessionContentAlignment)) _scrollBar = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.showScrollBar)) _copyOnSelect = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.copyOnSelect)) + _autoResume = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.autoResumeAgentSessions)) _hibernation = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.agentHibernationEnabled)) _idleSeconds = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.agentHibernationIdleSeconds)) _maxLive = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.agentHibernationMaxLiveTerminals)) @@ -77,6 +77,7 @@ public struct TerminalSection: View { sessionContentAlignment, scrollBar, copyOnSelect, + autoResume, hibernation, idleSeconds, maxLive, @@ -367,11 +368,18 @@ public struct TerminalSection: View { .accessibilityIdentifier("SettingsTerminalCopyOnSelectToggle") } SettingsCardDivider() - AgentRecoverySettingsRows( - defaultsStore: defaultsStore, - catalog: catalog, - hostActions: hostActions - ) + SettingsCardRow( + configurationReview: .json("terminal.autoResumeAgentSessions"), + String(localized: "settings.terminal.agentAutoResume", defaultValue: "Resume Agent Sessions on Reopen"), + subtitle: autoResume.current + ? String(localized: "settings.terminal.agentAutoResume.subtitleOn", defaultValue: "When cmux reopens after quit, restored agent terminals automatically run their resume command.") + : String(localized: "settings.terminal.agentAutoResume.subtitleOff", defaultValue: "When cmux reopens after quit, restored agent terminals stay idle until you resume them manually.") + ) { + Toggle("", isOn: Binding(get: { autoResume.current }, set: { autoResume.set($0) })) + .labelsHidden() + .controlSize(.small) + .accessibilityIdentifier("SettingsTerminalAgentAutoResumeToggle") + } SettingsCardDivider() SettingsCardRow( configurationReview: .json("terminal.agentHibernation.enabled"), diff --git a/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/AgentRecoverySettingsModelTests.swift b/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/AgentRecoverySettingsModelTests.swift deleted file mode 100644 index ffbc4c44062d..000000000000 --- a/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/AgentRecoverySettingsModelTests.swift +++ /dev/null @@ -1,63 +0,0 @@ -import CmuxSettings -import Foundation -import Testing - -@testable import CmuxSettingsUI - -@MainActor -@Suite("Agent recovery settings") -struct AgentRecoverySettingsModelTests { - @Test("auto-retry publishes its live signal only after persistence commits") - func autoRetrySignalsCommittedChange() async throws { - let suiteName = "AgentRecoverySettingsModelTests.\(UUID().uuidString)" - defer { UserDefaults(suiteName: suiteName)?.removePersistentDomain(forName: suiteName) } - let store = UserDefaultsSettingsStore( - defaults: UserDefaults(suiteName: suiteName)! - ) - let host = AgentRecoverySettingsHostActionsSpy() - let model = AgentRecoverySettingsModel( - defaultsStore: store, - catalog: SettingCatalog(), - hostActions: host - ) - - model.setAutoRetry(true) - await host.waitForAutoRetryChange() - - #expect(host.autoRetryChangeCount == 1) - #expect(await store.value(for: SettingCatalog().terminal.autoRetryAgentSessions)) - } -} - -@MainActor -private final class AgentRecoverySettingsHostActionsSpy: SettingsHostActions { - private(set) var autoRetryChangeCount = 0 - private var autoRetryChangeWaiters: [CheckedContinuation] = [] - - func agentSessionAutoRetrySettingDidChange() { - autoRetryChangeCount += 1 - let waiters = autoRetryChangeWaiters - autoRetryChangeWaiters.removeAll() - for waiter in waiters { - waiter.resume() - } - } - - func waitForAutoRetryChange() async { - guard autoRetryChangeCount == 0 else { return } - await withCheckedContinuation { continuation in - autoRetryChangeWaiters.append(continuation) - } - } - - func clearBrowserHistory() {} - func openConfigInExternalEditor() {} - func sendFeedback() {} - func sendTestNotification() {} - func openSystemNotificationSettings() {} - func restartApp() {} - func openBrowserImportFlow() {} - func requestNotificationAuthorization() {} - func openTerminalConfigWindow() {} - func previewNotificationSound(value: String, customFilePath: String) {} -} diff --git a/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift b/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift index 92f421613417..8fbc87182c93 100644 --- a/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift +++ b/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift @@ -126,7 +126,6 @@ struct SettingsRowAnchorResolutionTests { "terminal.rendererRealization.idleSeconds", "terminal.rendererRealization.maxWarmRenderers", "terminal.autoResumeAgentSessions", - "terminal.autoRetryAgentSessions", "terminal.copyOnSelect", "terminal.resumeCommands", "terminal.sessionContentAlignment", diff --git a/Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/AgentRetry/AgentSessionRetryContext.swift b/Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/AgentRetry/AgentSessionRetryContext.swift deleted file mode 100644 index b4d155628e79..000000000000 --- a/Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/AgentRetry/AgentSessionRetryContext.swift +++ /dev/null @@ -1,30 +0,0 @@ -/// The authoritative facts available when an agent command returns to its shell. -public struct AgentSessionRetryContext: Equatable, Sendable { - /// Whether the user opted into automatic retry. - public let isEnabled: Bool - /// Whether managed hooks observed an active agent during this command. - public let hadActiveAgentSession: Bool - /// Whether cmux retained a managed resume binding for the active session. - public let hasManagedResumeBinding: Bool - /// The shell-reported exit status, or `nil` when shell integration omitted it. - public let exitCode: Int? - - /// Creates the facts used to classify one command termination. - /// - /// - Parameters: - /// - isEnabled: Whether the user opted into automatic retry. - /// - hadActiveAgentSession: Whether managed hooks observed an active agent. - /// - hasManagedResumeBinding: Whether the active session can be resumed. - /// - exitCode: The shell-reported exit status, or `nil` when unavailable. - public init( - isEnabled: Bool, - hadActiveAgentSession: Bool, - hasManagedResumeBinding: Bool, - exitCode: Int? - ) { - self.isEnabled = isEnabled - self.hadActiveAgentSession = hadActiveAgentSession - self.hasManagedResumeBinding = hasManagedResumeBinding - self.exitCode = exitCode - } -} diff --git a/Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/AgentRetry/AgentSessionRetryDecision.swift b/Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/AgentRetry/AgentSessionRetryDecision.swift deleted file mode 100644 index 011f109f6e8f..000000000000 --- a/Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/AgentRetry/AgentSessionRetryDecision.swift +++ /dev/null @@ -1,9 +0,0 @@ -/// The next state transition selected by ``AgentSessionRetryPolicy``. -public enum AgentSessionRetryDecision: Equatable, Sendable { - /// Schedule the numbered retry after the specified delay. - case retry(attempt: Int, maximumAttempts: Int, delaySeconds: Double) - /// Surface exhaustion because every permitted retry was already launched. - case exhausted(maximumAttempts: Int) - /// Decline retry for a fail-closed classification reason. - case reject(AgentSessionRetryRejection) -} diff --git a/Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/AgentRetry/AgentSessionRetryPolicy.swift b/Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/AgentRetry/AgentSessionRetryPolicy.swift deleted file mode 100644 index 8d6605648585..000000000000 --- a/Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/AgentRetry/AgentSessionRetryPolicy.swift +++ /dev/null @@ -1,62 +0,0 @@ -/// Fail-closed retry classification and bounded exponential backoff. -public struct AgentSessionRetryPolicy: Equatable, Sendable { - /// The product retry policy: three attempts after 1, 2, and 4 seconds. - public static let standard = AgentSessionRetryPolicy( - maximumAttempts: 3, - backoffSeconds: [1, 2, 4] - ) - - /// The maximum resume commands permitted for one continuously failing session. - public let maximumAttempts: Int - /// Delay before each retry, indexed by the zero-based completed-attempt count. - public let backoffSeconds: [Double] - - /// Creates a bounded retry policy. - /// - /// - Parameters: - /// - maximumAttempts: A positive maximum number of resume commands. - /// - backoffSeconds: Nonnegative finite delays with at least one value per attempt. - public init(maximumAttempts: Int, backoffSeconds: [Double]) { - precondition(maximumAttempts > 0) - precondition(backoffSeconds.count >= maximumAttempts) - precondition(backoffSeconds.prefix(maximumAttempts).allSatisfy { $0.isFinite && $0 >= 0 }) - self.maximumAttempts = maximumAttempts - self.backoffSeconds = backoffSeconds - } - - /// Returns the next retry action after `completedAttempts` resume commands - /// have already been launched for the same failed session. - /// - /// - Parameters: - /// - context: The authoritative opt-in, lifecycle, binding, and exit facts. - /// - completedAttempts: Resume commands already launched for this session. - /// - Returns: The next retry, exhaustion, or rejection transition. - public func decision( - for context: AgentSessionRetryContext, - completedAttempts: Int - ) -> AgentSessionRetryDecision { - guard context.isEnabled else { return .reject(.disabled) } - guard context.hadActiveAgentSession else { return .reject(.inactiveSession) } - guard context.hasManagedResumeBinding else { return .reject(.missingResumeBinding) } - guard let exitCode = context.exitCode else { return .reject(.unknownExit) } - guard exitCode != 0 else { return .reject(.successfulExit) } - - // Shells conventionally encode signal termination as 128 + signal. - // Status 128 itself is also deliberately treated as ambiguous because - // wrappers/providers use it for interrupted or otherwise uncertain - // termination. False-positive resurrection is worse than skipping one - // retry, so reject the full conservative range. - guard !(128...192).contains(exitCode) else { return .reject(.signalExit) } - - let normalizedCompletedAttempts = max(0, completedAttempts) - guard normalizedCompletedAttempts < maximumAttempts else { - return .exhausted(maximumAttempts: maximumAttempts) - } - let attempt = normalizedCompletedAttempts + 1 - return .retry( - attempt: attempt, - maximumAttempts: maximumAttempts, - delaySeconds: backoffSeconds[attempt - 1] - ) - } -} diff --git a/Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/AgentRetry/AgentSessionRetryRejection.swift b/Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/AgentRetry/AgentSessionRetryRejection.swift deleted file mode 100644 index 0f6e77aaa792..000000000000 --- a/Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/AgentRetry/AgentSessionRetryRejection.swift +++ /dev/null @@ -1,15 +0,0 @@ -/// Why automatic retry deliberately declined a command termination. -public enum AgentSessionRetryRejection: Equatable, Sendable { - /// Automatic retry is disabled. - case disabled - /// No managed agent was active when the command ended. - case inactiveSession - /// The active session has no authoritative managed resume binding. - case missingResumeBinding - /// Shell integration did not report an exit status. - case unknownExit - /// The command completed successfully. - case successfulExit - /// The exit status is signal-like and may represent an intentional stop. - case signalExit -} diff --git a/Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/AgentRetry/AgentSessionRetryPolicyTests.swift b/Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/AgentRetry/AgentSessionRetryPolicyTests.swift deleted file mode 100644 index ad9c503b692a..000000000000 --- a/Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/AgentRetry/AgentSessionRetryPolicyTests.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Testing -@testable import CmuxWorkspaces - -@Suite("Agent session retry policy") -struct AgentSessionRetryPolicyTests { - private let policy = AgentSessionRetryPolicy.standard - - @Test( - "non-error and ambiguous terminations fail closed", - arguments: [ - (false, true, true, Optional(1), AgentSessionRetryRejection.disabled), - (true, false, true, Optional(1), .inactiveSession), - (true, true, false, Optional(1), .missingResumeBinding), - (true, true, true, nil, .unknownExit), - (true, true, true, Optional(0), .successfulExit), - (true, true, true, Optional(128), .signalExit), - (true, true, true, Optional(130), .signalExit), - (true, true, true, Optional(143), .signalExit), - ] - ) - func rejectsUnsafeTermination( - isEnabled: Bool, - hadActiveSession: Bool, - hasResumeBinding: Bool, - exitCode: Int?, - expected: AgentSessionRetryRejection - ) { - let decision = policy.decision( - for: AgentSessionRetryContext( - isEnabled: isEnabled, - hadActiveAgentSession: hadActiveSession, - hasManagedResumeBinding: hasResumeBinding, - exitCode: exitCode - ), - completedAttempts: 0 - ) - - #expect(decision == .reject(expected)) - } - - @Test("qualifying failures receive bounded exponential backoff") - func qualifyingFailuresBackOff() { - let context = AgentSessionRetryContext( - isEnabled: true, - hadActiveAgentSession: true, - hasManagedResumeBinding: true, - exitCode: 1 - ) - - #expect(policy.decision(for: context, completedAttempts: 0) == - .retry(attempt: 1, maximumAttempts: 3, delaySeconds: 1)) - #expect(policy.decision(for: context, completedAttempts: 1) == - .retry(attempt: 2, maximumAttempts: 3, delaySeconds: 2)) - #expect(policy.decision(for: context, completedAttempts: 2) == - .retry(attempt: 3, maximumAttempts: 3, delaySeconds: 4)) - #expect(policy.decision(for: context, completedAttempts: 3) == - .exhausted(maximumAttempts: 3)) - } - - @Test("non-signal nonzero exits remain eligible") - func nonSignalErrorsRemainEligible() { - for exitCode in [1, 5, 65, 127, 193, 255] { - let context = AgentSessionRetryContext( - isEnabled: true, - hadActiveAgentSession: true, - hasManagedResumeBinding: true, - exitCode: exitCode - ) - #expect(policy.decision(for: context, completedAttempts: 0) == - .retry(attempt: 1, maximumAttempts: 3, delaySeconds: 1)) - } - } -} diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index bfa1d47aa141..f6e63574c954 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -1361,142 +1361,6 @@ } } }, - "agent.autoRetry.log.exhausted": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Agent exited with code %lld after %lld retries; automatic retries exhausted." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "エージェントは %2$lld 回の再試行後にコード %1$lld で終了しました。自動再試行の上限に達しました。" - } - } - } - }, - "agent.autoRetry.log.launchFailed": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "The agent resume command could not be sent; automatic retries stopped." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "エージェントの再開コマンドを送信できなかったため、自動再試行を停止しました。" - } - } - } - }, - "agent.autoRetry.log.scheduled": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Agent exited with code %lld; retrying (attempt %lld/%lld)." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "エージェントはコード %1$lld で終了しました。再試行します(%2$lld/%3$lld 回目)。" - } - } - } - }, - "agent.autoRetry.notification.exhausted.body.one": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "The agent could not be resumed after 1 automatic retry." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "1 回の自動再試行後もエージェントを再開できませんでした。" - } - } - } - }, - "agent.autoRetry.notification.exhausted.body.other": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "The agent could not be resumed after %lld automatic retries." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "%lld 回の自動再試行後もエージェントを再開できませんでした。" - } - } - } - }, - "agent.autoRetry.notification.exhausted.title": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Agent retries exhausted" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "エージェントの再試行上限に到達" - } - } - } - }, - "agent.autoRetry.status.exhausted": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Agent auto-retry exhausted" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "エージェントの自動再試行上限に到達" - } - } - } - }, - "agent.autoRetry.status.retrying": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Retrying agent (attempt %lld/%lld)…" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "エージェントを再試行中(%1$lld/%2$lld 回目)…" - } - } - } - }, "agent.campfire.capability.fallback": { "extractionState": "manual", "localizations": { @@ -188642,23 +188506,6 @@ } } }, - "settings.search.alias.setting.terminal.agent-auto-retry": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "terminal.autoRetryAgentSessions auto retry resume error failure transport api rate limit overloaded agents claude codex" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "terminal.autoRetryAgentSessions 自動 再試行 再開 エラー 失敗 通信 API レート制限 過負荷 エージェント claude codex" - } - } - } - }, "settings.search.alias.setting.terminal.copy-on-select": { "extractionState": "manual", "localizations": { @@ -194310,57 +194157,6 @@ } } }, - "settings.terminal.agentAutoRetry": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Retry Failed Agent Sessions" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "失敗したエージェントセッションを再試行" - } - } - } - }, - "settings.terminal.agentAutoRetry.subtitleOff": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Errored agent sessions stay stopped until you resume them manually." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "エラーで停止したエージェントセッションは、手動で再開するまで停止したままになります。" - } - } - } - }, - "settings.terminal.agentAutoRetry.subtitleOn": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Eligible managed agent sessions that fail during active work resume automatically with bounded backoff; normal and intentional exits are not retried." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "作業中に失敗した再試行対象となる管理エージェントセッションのみを、回数制限付きバックオフで自動的に再開します。正常終了や意図的な終了は再試行しません。" - } - } - } - }, "settings.terminal.agentAutoResume": { "extractionState": "manual", "localizations": { @@ -227309,23 +227105,6 @@ } } }, - "socket.surface.resume.agentSessionEndedMustBeBoolean": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "agent_session_ended must be a boolean" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "agent_session_ended はブール値である必要があります" - } - } - } - }, "socket.surfaceSplitOff.error.appDelegateUnavailable": { "extractionState": "manual", "localizations": { diff --git a/Sources/AgentSessionRetryCoordinator+Scheduling.swift b/Sources/AgentSessionRetryCoordinator+Scheduling.swift deleted file mode 100644 index 0382276d1832..000000000000 --- a/Sources/AgentSessionRetryCoordinator+Scheduling.swift +++ /dev/null @@ -1,165 +0,0 @@ -import Foundation -import OSLog - -extension AgentSessionRetryCoordinator { - func scheduleRetry( - panelId: UUID, - binding: SurfaceResumeBindingSnapshot, - commandGeneration: UInt64, - exitCode: Int, - attempt: Int, - maximumAttempts: Int, - delaySeconds: TimeInterval - ) { - statesByPanelId[panelId]?.timer?.invalidate() - let timer = Timer(timeInterval: delaySeconds, repeats: false) { [weak self] _ in - Task { @MainActor [weak self] in - self?.retryTimerFired(panelId: panelId) - } - } - statesByPanelId[panelId] = RetryState( - completedAttempts: attempt - 1, - binding: binding, - commandGeneration: commandGeneration, - phase: .waiting( - attempt: attempt, - maximumAttempts: maximumAttempts, - exitCode: exitCode - ), - timer: timer - ) - RunLoop.main.add(timer, forMode: .common) - - workspace?.showAgentRetryScheduled( - panelId: panelId, - exitCode: exitCode, - attempt: attempt, - maximumAttempts: maximumAttempts - ) - Self.logger.warning( - "Agent session exited abnormally; scheduling retry panel=\(panelId, privacy: .public) exit=\(exitCode) attempt=\(attempt)/\(maximumAttempts) delaySeconds=\(delaySeconds)" - ) - } - - func retryTimerFired(panelId: UUID) { - guard workspace != nil, - let state = statesByPanelId[panelId] else { - reset(panelId: panelId) - return - } - guard case let .waiting(attempt, maximumAttempts, _) = state.phase else { - return - } - guard settings.isEnabled, - commandGenerationsByPanelId[panelId] == state.commandGeneration else { - reset(panelId: panelId) - return - } - - state.timer?.invalidate() - let readinessTimer = Timer( - timeInterval: Self.readinessTimeoutSeconds, - repeats: false - ) { [weak self] _ in - Task { @MainActor [weak self] in - self?.retryReadinessDeadlineFired(panelId: panelId) - } - } - statesByPanelId[panelId] = RetryState( - completedAttempts: state.completedAttempts, - binding: state.binding, - commandGeneration: state.commandGeneration, - phase: .ready(attempt: attempt, maximumAttempts: maximumAttempts), - timer: readinessTimer - ) - RunLoop.main.add(readinessTimer, forMode: .common) - attemptReadyRetry(panelId: panelId) - } - - func retryReadinessDeadlineFired(panelId: UUID) { - guard case .ready = statesByPanelId[panelId]?.phase else { return } - Self.logger.warning( - "Agent retry cancelled while waiting for authoritative shell idle panel=\(panelId, privacy: .public)" - ) - reset(panelId: panelId) - } - - func attemptReadyRetry(panelId: UUID) { - guard let workspace, - let state = statesByPanelId[panelId], - case let .ready(attempt, maximumAttempts) = state.phase else { - return - } - guard settings.isEnabled, - commandGenerationsByPanelId[panelId] == state.commandGeneration else { - reset(panelId: panelId) - return - } - switch workspace.managedAgentRetryLaunchReadiness( - binding: state.binding, - panelId: panelId - ) { - case .awaitingIdle: - return - case .invalidated: - reset(panelId: panelId) - return - case .ready: - break - } - - state.timer?.invalidate() - let accepted = workspace.sendManagedAgentRetry( - binding: state.binding, - panelId: panelId, - beforeSending: { [weak self] in - self?.retryInputInjectionPanelIds.insert(panelId) - }, - afterSending: { [weak self] in - self?.retryInputInjectionPanelIds.remove(panelId) - } - ) - guard accepted else { - workspace.showAgentRetryLaunchFailure(panelId: panelId) - statesByPanelId[panelId] = RetryState( - completedAttempts: attempt, - binding: state.binding, - commandGeneration: state.commandGeneration, - phase: .exhausted(maximumAttempts: maximumAttempts), - timer: nil - ) - Self.logger.error( - "Agent retry resume command was not accepted panel=\(panelId, privacy: .public) attempt=\(attempt)/\(maximumAttempts)" - ) - return - } - - let launchTimer = Timer( - timeInterval: Self.launchAcknowledgementTimeoutSeconds, - repeats: false - ) { [weak self] _ in - Task { @MainActor [weak self] in - self?.retryLaunchAcknowledgementDeadlineFired(panelId: panelId) - } - } - statesByPanelId[panelId] = RetryState( - completedAttempts: attempt, - binding: state.binding, - commandGeneration: state.commandGeneration, - phase: .awaitingLaunch(attempt: attempt, maximumAttempts: maximumAttempts), - timer: launchTimer - ) - RunLoop.main.add(launchTimer, forMode: .common) - Self.logger.info( - "Agent retry resume command sent panel=\(panelId, privacy: .public) attempt=\(attempt)/\(maximumAttempts)" - ) - } - - func retryLaunchAcknowledgementDeadlineFired(panelId: UUID) { - guard case .awaitingLaunch = statesByPanelId[panelId]?.phase else { return } - Self.logger.warning( - "Agent retry cancelled while waiting for launch acknowledgement panel=\(panelId, privacy: .public)" - ) - reset(panelId: panelId) - } -} diff --git a/Sources/AgentSessionRetryCoordinator+SurfaceTransfer.swift b/Sources/AgentSessionRetryCoordinator+SurfaceTransfer.swift deleted file mode 100644 index b47a3478be33..000000000000 --- a/Sources/AgentSessionRetryCoordinator+SurfaceTransfer.swift +++ /dev/null @@ -1,62 +0,0 @@ -import CmuxWorkspaces -import Foundation - -extension AgentSessionRetryCoordinator { - /// Captures proof that a transferred pane's managed checkpoint owns its command. - func transferredCompletedAttempts( - panelId: UUID, - shellActivityState: PanelShellActivityState?, - binding: SurfaceResumeBindingSnapshot? - ) -> Int? { - guard settings.isEnabled, - shellActivityState == .commandRunning, - let binding, - let commandGeneration = commandGenerationsByPanelId[panelId], - let ownedRun = managedRunsByPanelId[panelId], - ownedRun.commandGeneration == commandGeneration, - ownedRun.binding.isSameManagedSession(as: binding) else { - return nil - } - return statesByPanelId[panelId]?.completedAttempts ?? 0 - } - - /// Reconstructs transferred command ownership; missing or stale facts fail closed. - func seedTransferredManagedRun( - panelId: UUID, - shellActivityState: PanelShellActivityState?, - binding: SurfaceResumeBindingSnapshot?, - completedAttempts: Int? - ) { - guard settings.isEnabled, - shellActivityState == .commandRunning, - let binding, - binding.isAgentHookBinding, - binding.checkpointId?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false, - binding.kind?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false, - let completedAttempts, - completedAttempts >= 0, - completedAttempts <= policy.maximumAttempts else { - cancel(panelId: panelId) - return - } - - let commandGeneration = (commandGenerationsByPanelId[panelId] ?? 0) &+ 1 - commandGenerationsByPanelId[panelId] = commandGeneration - managedRunsByPanelId[panelId] = ManagedRunOwnership( - binding: binding, - commandGeneration: commandGeneration - ) - if completedAttempts > 0 { - statesByPanelId[panelId] = RetryState( - completedAttempts: completedAttempts, - binding: binding, - commandGeneration: commandGeneration, - phase: .running( - attempt: completedAttempts, - maximumAttempts: policy.maximumAttempts - ), - timer: nil - ) - } - } -} diff --git a/Sources/AgentSessionRetryCoordinator.swift b/Sources/AgentSessionRetryCoordinator.swift deleted file mode 100644 index b46068797803..000000000000 --- a/Sources/AgentSessionRetryCoordinator.swift +++ /dev/null @@ -1,478 +0,0 @@ -import CmuxSidebar -import CmuxWorkspaces -import Foundation -import Observation -import OSLog - -@MainActor -@Observable -final class AgentSessionRetryCoordinator { - typealias RetryState = AgentSessionRetryPanelState - - struct ManagedRunOwnership { - let binding: SurfaceResumeBindingSnapshot - let commandGeneration: UInt64 - } - - @ObservationIgnored weak var workspace: Workspace? - @ObservationIgnored let policy: AgentSessionRetryPolicy - @ObservationIgnored let settings: AgentSessionAutoRetrySettings - @ObservationIgnored private var settingsDidChangeObserver: NSObjectProtocol? - @ObservationIgnored var commandGenerationsByPanelId: [UUID: UInt64] = [:] - @ObservationIgnored var managedRunsByPanelId: [UUID: ManagedRunOwnership] = [:] - @ObservationIgnored private var endedSessionCandidatesByPanelId: [UUID: ManagedRunOwnership] = [:] - @ObservationIgnored private var pendingManagedStartsByPanelId: [UUID: SurfaceResumeBindingSnapshot] = [:] - @ObservationIgnored var statesByPanelId: [UUID: RetryState] = [:] - @ObservationIgnored var retryInputInjectionPanelIds: Set = [] - - init( - workspace: Workspace, - policy: AgentSessionRetryPolicy = .standard, - settings: AgentSessionAutoRetrySettings - ) { - self.workspace = workspace - self.policy = policy - self.settings = settings - self.settingsDidChangeObserver = nil - self.settingsDidChangeObserver = settings.observeDidChange { [weak self] in - guard let self, !self.settings.isEnabled else { return } - self.cancelAll() - } - } - - deinit { - for state in statesByPanelId.values { - state.timer?.invalidate() - } - if let settingsDidChangeObserver { - settings.removeDidChangeObserver(settingsDidChangeObserver) - } - } - - func agentLifecycleDidChange( - panelId: UUID, - lifecycle: AgentHibernationLifecycleState - ) { - switch lifecycle { - case .running, .needsInput: - // Lifecycle keys do not carry a session id. Bind them to a managed - // run only while its exact resume checkpoint owns the current shell - // command. A replay for an ended generation must not erase the - // command-finished candidate. - bindCurrentManagedRunIfPossible(panelId: panelId) - recordHookFirstManagedStartIfPossible(panelId: panelId) - acknowledgeInjectedRetryFromHooksIfPossible(panelId: panelId) - case .idle: - if workspace?.hasActiveAgentLifecycleForRetry(panelId: panelId) != true { - pendingManagedStartsByPanelId.removeValue(forKey: panelId) - } - if case .running = statesByPanelId[panelId]?.phase, - managedRunsByPanelId[panelId] == nil, - endedSessionCandidatesByPanelId[panelId] == nil, - workspace?.panelShellActivityStates[panelId] != .commandRunning { - reset(panelId: panelId) - return - } - attemptReadyRetry(panelId: panelId) - if statesByPanelId[panelId] == nil, - managedRunsByPanelId[panelId] == nil, - endedSessionCandidatesByPanelId[panelId] == nil, - commandGenerationsByPanelId[panelId] == nil { - clearRecovery(panelId: panelId) - } - case .unknown: - break - } - } - - func managedResumeBindingDidChange( - panelId: UUID, - binding: SurfaceResumeBindingSnapshot? - ) { - guard let binding, - binding.isAgentHookBinding, - binding.checkpointId?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false, - binding.kind?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false else { - clearRecovery(panelId: panelId) - return - } - - // Any binding publication after a command was classified or ended is - // live-session evidence that makes injection ambiguous. The next real - // shell command will establish a fresh generation. - if endedSessionCandidatesByPanelId[panelId] != nil || - statesByPanelId[panelId]?.phase.isWaitingOrReadyOrExhausted == true { - clearRecovery(panelId: panelId) - return - } - - if let state = statesByPanelId[panelId] { - switch state.phase { - case .awaitingLaunch, .running: - guard state.binding.isSameManagedSession(as: binding) else { - clearRecovery(panelId: panelId) - return - } - var updatedState = state - updatedState.binding = binding - statesByPanelId[panelId] = updatedState - case .waiting, .ready, .exhausted: - // A different managed session, or a manual resume while waiting - // or exhausted, owns the pane now and starts with a fresh budget. - clearRecovery(panelId: panelId) - return - } - } - bindCurrentManagedRunIfPossible(panelId: panelId) - recordHookFirstManagedStartIfPossible(panelId: panelId) - acknowledgeInjectedRetryFromHooksIfPossible( - panelId: panelId, - bindingPublicationIsEvidence: true - ) - } - - func managedResumeBindingDidClear( - panelId: UUID, - binding: SurfaceResumeBindingSnapshot, - sessionDidEnd: Bool - ) { - if let state = statesByPanelId[panelId], - state.phase.isWaitingOrReadyOrExhausted, - state.binding.isSameManagedSession(as: binding), - state.commandGeneration == commandGenerationsByPanelId[panelId], - sessionDidEnd { - // command-finished may be delivered before the hook's socket clear. - // The already-classified generation remains authoritative. - attemptReadyRetry(panelId: panelId) - return - } - - guard settings.isEnabled, - sessionDidEnd, - binding.isAgentHookBinding, - binding.checkpointId?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false, - binding.kind?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false, - let ownedRun = managedRunsByPanelId[panelId], - ownedRun.binding.isSameManagedSession(as: binding), - ownedRun.commandGeneration == commandGenerationsByPanelId[panelId] else { - clearRecovery(panelId: panelId) - return - } - endedSessionCandidatesByPanelId[panelId] = ManagedRunOwnership( - binding: binding, - commandGeneration: ownedRun.commandGeneration - ) - managedRunsByPanelId.removeValue(forKey: panelId) - } - - func agentLifecycleDidClear(panelId: UUID) { - if workspace?.hasActiveAgentLifecycleForRetry(panelId: panelId) != true { - pendingManagedStartsByPanelId.removeValue(forKey: panelId) - } - attemptReadyRetry(panelId: panelId) - guard workspace?.hasActiveAgentLifecycleForRetry(panelId: panelId) != true, - statesByPanelId[panelId] == nil, - managedRunsByPanelId[panelId] == nil, - endedSessionCandidatesByPanelId[panelId] == nil else { - return - } - clearRecovery(panelId: panelId) - } - - func shellActivityDidChange(panelId: UUID, state: PanelShellActivityState) { - guard state == .commandRunning else { - if state == .promptIdle { - preserveOwnedRunAtPromptIdle(panelId: panelId) - attemptReadyRetry(panelId: panelId) - } - return - } - let pendingManagedStart = pendingManagedStartsByPanelId.removeValue(forKey: panelId) - let nextGeneration = (commandGenerationsByPanelId[panelId] ?? 0) &+ 1 - - var retainedInjectedRetryBinding: SurfaceResumeBindingSnapshot? - if var retryState = statesByPanelId[panelId] { - switch retryState.phase { - case let .awaitingLaunch(attempt, maximumAttempts), - let .running(attempt, maximumAttempts): - retryState.timer?.invalidate() - retryState.commandGeneration = nextGeneration - retryState.phase = .running( - attempt: attempt, - maximumAttempts: maximumAttempts - ) - retryState.timer = nil - statesByPanelId[panelId] = retryState - retainedInjectedRetryBinding = retryState.binding - case .waiting, .ready, .exhausted: - clearRecovery(panelId: panelId) - } - } else { - // A user-started command supersedes every unconsumed completion and - // every scheduled/exhausted retry for the prior generation. - clearRecovery(panelId: panelId) - } - if let retainedInjectedRetryBinding { - managedRunsByPanelId.removeValue(forKey: panelId) - endedSessionCandidatesByPanelId.removeValue(forKey: panelId) - managedRunsByPanelId[panelId] = ManagedRunOwnership( - binding: retainedInjectedRetryBinding, - commandGeneration: nextGeneration - ) - workspace?.removeAgentRetryStatusEntry(panelId: panelId) - } - commandGenerationsByPanelId[panelId] = nextGeneration - - if retainedInjectedRetryBinding == nil, - let pendingManagedStart, - let workspace, - workspace.hasActiveAgentLifecycleForRetry(panelId: panelId), - workspace.managedAgentRetryBinding(panelId: panelId)?.isSameManagedSession( - as: pendingManagedStart - ) == true { - managedRunsByPanelId[panelId] = ManagedRunOwnership( - binding: pendingManagedStart, - commandGeneration: nextGeneration - ) - } - } - - func commandFinished(panelId: UUID, exitCode: Int?) { - guard workspace != nil else { return } - if statesByPanelId[panelId]?.phase.isWaitingOrReadyOrExhausted == true { return } - guard let commandGeneration = commandGenerationsByPanelId[panelId] else { - clearRecovery(panelId: panelId) - return - } - let candidate: ManagedRunOwnership - if let state = statesByPanelId[panelId], - case .awaitingLaunch = state.phase, - state.commandGeneration == commandGeneration { - // The accepted retry injection is itself authoritative ownership. - // Explicit input and the acknowledgement deadline both cancel this - // phase, so a command-finished event that wins the start-hook race - // belongs to the injected resume attempt. - candidate = ManagedRunOwnership( - binding: state.binding, - commandGeneration: commandGeneration - ) - } else if let ownedCandidate = commandFinishedCandidate( - panelId: panelId, - commandGeneration: commandGeneration - ) { - candidate = ownedCandidate - } else { - clearRecovery(panelId: panelId) - return - } - let completedAttempts = statesByPanelId[panelId]?.completedAttempts ?? 0 - let context = AgentSessionRetryContext( - isEnabled: settings.isEnabled, - hadActiveAgentSession: true, - hasManagedResumeBinding: true, - exitCode: exitCode - ) - - switch policy.decision(for: context, completedAttempts: completedAttempts) { - case let .retry(attempt, maximumAttempts, delaySeconds): - guard let exitCode else { - reset(panelId: panelId) - return - } - scheduleRetry( - panelId: panelId, - binding: candidate.binding, - commandGeneration: commandGeneration, - exitCode: exitCode, - attempt: attempt, - maximumAttempts: maximumAttempts, - delaySeconds: delaySeconds - ) - case let .exhausted(maximumAttempts): - markExhausted( - panelId: panelId, - exitCode: exitCode, - maximumAttempts: maximumAttempts, - binding: candidate.binding, - commandGeneration: commandGeneration - ) - case .reject: - clearRecovery(panelId: panelId) - } - } - - /// Cancels a delayed retry before user input can share its shell prompt. - func explicitTerminalInputDidBegin(panelId: UUID) { - guard !retryInputInjectionPanelIds.contains(panelId) else { return } - let promptIsIdle = workspace?.panelShellActivityStates[panelId] == .promptIdle - let hasUnclassifiedRun = managedRunsByPanelId[panelId] != nil || - endedSessionCandidatesByPanelId[panelId] != nil || - pendingManagedStartsByPanelId[panelId] != nil - guard statesByPanelId[panelId]?.phase.isPendingOrExhausted == true || - (promptIsIdle && hasUnclassifiedRun) else { return } - reset(panelId: panelId) - } - - func cancel(panelId: UUID) { - reset(panelId: panelId) - } - - func cancelAll() { - for state in statesByPanelId.values { - state.timer?.invalidate() - } - statesByPanelId.removeAll() - commandGenerationsByPanelId.removeAll() - managedRunsByPanelId.removeAll() - endedSessionCandidatesByPanelId.removeAll() - pendingManagedStartsByPanelId.removeAll() - workspace?.removeAllAgentRetryStatusEntries() - } - - private func markExhausted( - panelId: UUID, - exitCode: Int?, - maximumAttempts: Int, - binding: SurfaceResumeBindingSnapshot?, - commandGeneration: UInt64 - ) { - let retainedBinding = binding ?? statesByPanelId[panelId]?.binding - guard let retainedBinding else { - reset(panelId: panelId) - return - } - statesByPanelId[panelId]?.timer?.invalidate() - statesByPanelId[panelId] = RetryState( - completedAttempts: maximumAttempts, - binding: retainedBinding, - commandGeneration: commandGeneration, - phase: .exhausted(maximumAttempts: maximumAttempts), - timer: nil - ) - workspace?.showAgentRetriesExhausted( - panelId: panelId, - exitCode: exitCode, - maximumAttempts: maximumAttempts - ) - Self.logger.error( - "Agent automatic retries exhausted panel=\(panelId, privacy: .public) exit=\(exitCode ?? -1) attempts=\(maximumAttempts)" - ) - } - - func reset(panelId: UUID) { - clearRecovery(panelId: panelId) - commandGenerationsByPanelId.removeValue(forKey: panelId) - } - - private func clearRecovery(panelId: UUID) { - statesByPanelId.removeValue(forKey: panelId)?.timer?.invalidate() - managedRunsByPanelId.removeValue(forKey: panelId) - endedSessionCandidatesByPanelId.removeValue(forKey: panelId) - pendingManagedStartsByPanelId.removeValue(forKey: panelId) - workspace?.removeAgentRetryStatusEntry(panelId: panelId) - } - - private func recordHookFirstManagedStartIfPossible(panelId: UUID) { - guard let workspace, - workspace.panelShellActivityStates[panelId] != .commandRunning, - statesByPanelId[panelId] == nil, - managedRunsByPanelId[panelId] == nil, - endedSessionCandidatesByPanelId[panelId] == nil, - workspace.hasActiveAgentLifecycleForRetry(panelId: panelId), - let binding = workspace.managedAgentRetryBinding(panelId: panelId) else { - return - } - pendingManagedStartsByPanelId[panelId] = binding - } - - private func acknowledgeInjectedRetryFromHooksIfPossible( - panelId: UUID, - bindingPublicationIsEvidence: Bool = false - ) { - guard let workspace, - var state = statesByPanelId[panelId], - case let .awaitingLaunch(attempt, maximumAttempts) = state.phase, - let binding = workspace.managedAgentRetryBinding(panelId: panelId), - state.binding.isSameManagedSession(as: binding), - bindingPublicationIsEvidence || - workspace.hasActiveAgentLifecycleForRetry(panelId: panelId) else { - return - } - state.timer?.invalidate() - state.binding = binding - state.phase = .running(attempt: attempt, maximumAttempts: maximumAttempts) - state.timer = nil - statesByPanelId[panelId] = state - pendingManagedStartsByPanelId[panelId] = binding - workspace.removeAgentRetryStatusEntry(panelId: panelId) - } - - private func preserveOwnedRunAtPromptIdle(panelId: UUID) { - guard endedSessionCandidatesByPanelId[panelId] == nil, - let workspace, - let commandGeneration = commandGenerationsByPanelId[panelId], - let ownedRun = managedRunsByPanelId[panelId], - ownedRun.commandGeneration == commandGeneration else { - return - } - if let currentBinding = workspace.managedAgentRetryBinding(panelId: panelId), - !ownedRun.binding.isSameManagedSession(as: currentBinding) { - clearRecovery(panelId: panelId) - return - } - endedSessionCandidatesByPanelId[panelId] = ownedRun - managedRunsByPanelId.removeValue(forKey: panelId) - } - - private func bindCurrentManagedRunIfPossible( - panelId: UUID, - shellIsRunning: Bool? = nil - ) { - guard let workspace, - shellIsRunning ?? (workspace.panelShellActivityStates[panelId] == .commandRunning), - workspace.hasActiveAgentLifecycleForRetry(panelId: panelId), - let binding = workspace.managedAgentRetryBinding(panelId: panelId), - let commandGeneration = commandGenerationsByPanelId[panelId] else { - return - } - managedRunsByPanelId[panelId] = ManagedRunOwnership( - binding: binding, - commandGeneration: commandGeneration - ) - } - - private func commandFinishedCandidate( - panelId: UUID, - commandGeneration: UInt64 - ) -> ManagedRunOwnership? { - if let ended = endedSessionCandidatesByPanelId.removeValue(forKey: panelId), - ended.commandGeneration == commandGeneration { - return ended - } - guard let workspace, - let ownedRun = managedRunsByPanelId.removeValue(forKey: panelId), - ownedRun.commandGeneration == commandGeneration, - workspace.managedAgentRetryBinding(panelId: panelId)?.isSameManagedSession( - as: ownedRun.binding - ) == true else { - return nil - } - return ownedRun - } - - static let logger = Logger( - subsystem: "com.cmuxterm.app", - category: "AgentSessionAutoRetry" - ) - static let readinessTimeoutSeconds: TimeInterval = 30 - static let launchAcknowledgementTimeoutSeconds: TimeInterval = 30 -} - -extension SurfaceResumeBindingSnapshot { - func isSameManagedSession(as other: SurfaceResumeBindingSnapshot) -> Bool { - source == "agent-hook" && - other.source == "agent-hook" && - kind == other.kind && - checkpointId == other.checkpointId - } -} diff --git a/Sources/AgentSessionRetryPanelState.swift b/Sources/AgentSessionRetryPanelState.swift deleted file mode 100644 index 97fad5991fb4..000000000000 --- a/Sources/AgentSessionRetryPanelState.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation - -/// Per-pane retry budget and timer state retained across one managed session. -struct AgentSessionRetryPanelState { - enum Phase: Equatable { - case waiting(attempt: Int, maximumAttempts: Int, exitCode: Int) - case ready(attempt: Int, maximumAttempts: Int) - case awaitingLaunch(attempt: Int, maximumAttempts: Int) - case running(attempt: Int, maximumAttempts: Int) - case exhausted(maximumAttempts: Int) - - var isPendingOrExhausted: Bool { - switch self { - case .waiting, .ready, .awaitingLaunch, .exhausted: - true - case .running: - false - } - } - - var isWaitingOrReadyOrExhausted: Bool { - switch self { - case .waiting, .ready, .exhausted: - true - case .awaitingLaunch, .running: - false - } - } - } - - var completedAttempts: Int - var binding: SurfaceResumeBindingSnapshot - var commandGeneration: UInt64 - var phase: Phase - var timer: Timer? -} diff --git a/Sources/App/AgentSessionAutoRetrySettings.swift b/Sources/App/AgentSessionAutoRetrySettings.swift deleted file mode 100644 index a792dcfd3c23..000000000000 --- a/Sources/App/AgentSessionAutoRetrySettings.swift +++ /dev/null @@ -1,76 +0,0 @@ -import CmuxSettings -import Foundation - -struct AgentSessionAutoRetrySettings { - static var autoRetryAgentSessionsKey: String { - TerminalCatalogSection().autoRetryAgentSessions.userDefaultsKey - } - - static var defaultAutoRetryAgentSessions: Bool { - TerminalCatalogSection().autoRetryAgentSessions.defaultValue - } - static let didChangeNotification = Notification.Name("cmux.agentSessionAutoRetrySettingsDidChange") - - private let defaults: UserDefaults - private let notificationCenter: NotificationCenter - private let key: DefaultsKey - - init( - defaults: UserDefaults = .standard, - notificationCenter: NotificationCenter = .default, - key: DefaultsKey = TerminalCatalogSection().autoRetryAgentSessions - ) { - self.defaults = defaults - self.notificationCenter = notificationCenter - self.key = key - } - - var isEnabled: Bool { - guard defaults.object(forKey: key.userDefaultsKey) != nil else { - return key.defaultValue - } - return defaults.bool(forKey: key.userDefaultsKey) - } - - func setEnabled(_ enabled: Bool) { - let wasEnabled = isEnabled - defaults.set(enabled, forKey: key.userDefaultsKey) - if wasEnabled != enabled { - notifyDidChange() - } - } - - @discardableResult - func reset() -> Bool { - let wasEnabled = isEnabled - defaults.removeObject(forKey: key.userDefaultsKey) - let didChange = wasEnabled != isEnabled - if didChange { - notifyDidChange() - } - return didChange - } - - func notifyDidChange() { - notificationCenter.post(name: Self.didChangeNotification, object: nil) - } - - @MainActor - func observeDidChange( - _ handler: @escaping @MainActor @Sendable () -> Void - ) -> NSObjectProtocol { - notificationCenter.addObserver( - forName: Self.didChangeNotification, - object: nil, - queue: .main - ) { _ in - MainActor.assumeIsolated { - handler() - } - } - } - - func removeDidChangeObserver(_ observer: NSObjectProtocol) { - notificationCenter.removeObserver(observer) - } -} diff --git a/Sources/CmuxSettingsJSONPathSupport.swift b/Sources/CmuxSettingsJSONPathSupport.swift index 1e87280c2a37..024951974862 100644 --- a/Sources/CmuxSettingsJSONPathSupport.swift +++ b/Sources/CmuxSettingsJSONPathSupport.swift @@ -229,11 +229,6 @@ enum TerminalSettingsFileMapping { defaultsKey: AgentSessionAutoResumeSettings.autoResumeAgentSessionsKey, invalidPath: "terminal.autoResumeAgentSessions" ), - .init( - jsonKey: "autoRetryAgentSessions", - defaultsKey: AgentSessionAutoRetrySettings.autoRetryAgentSessionsKey, - invalidPath: "terminal.autoRetryAgentSessions" - ), ] } @@ -423,7 +418,6 @@ extension CmuxSettingsFileStore { "terminal.scrollSpeed", "terminal.copyOnSelect", "terminal.autoResumeAgentSessions", - "terminal.autoRetryAgentSessions", "terminal.showTextBoxOnNewTerminals", "terminal.focusTextBoxOnNewTerminals", "terminal.textBoxDefaultSubmitAction", diff --git a/Sources/CommandPalette/CommandPaletteSettingsToggle+AgentSessionRetry.swift b/Sources/CommandPalette/CommandPaletteSettingsToggle+AgentSessionRetry.swift deleted file mode 100644 index 45b0dea4dc1f..000000000000 --- a/Sources/CommandPalette/CommandPaletteSettingsToggle+AgentSessionRetry.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation - -extension CommandPaletteSettingsToggleCommands { - static func agentSessionAutoRetryDescriptor( - sectionTitle: @escaping @Sendable () -> String - ) -> CommandPaletteSettingToggleDescriptor { - CommandPaletteSettingToggleDescriptor( - commandId: commandIdPrefix + "autoRetryAgentSessions", - settingsKey: "terminal.autoRetryAgentSessions", - title: { - String( - localized: "settings.terminal.agentAutoRetry", - defaultValue: "Retry Failed Agent Sessions" - ) - }, - sectionTitle: sectionTitle, - keywords: ["terminal.autoRetryAgentSessions", "terminal", "agent", "retry", "resume", "error", "failure"], - isOn: { defaults in - AgentSessionAutoRetrySettings(defaults: defaults).isEnabled - }, - setOn: { newValue, defaults, notificationCenter in - AgentSessionAutoRetrySettings( - defaults: defaults, - notificationCenter: notificationCenter - ).setEnabled(newValue) - } - ) - } -} diff --git a/Sources/CommandPalette/CommandPaletteSettingsToggle.swift b/Sources/CommandPalette/CommandPaletteSettingsToggle.swift index 1542ac535d4e..f3b739adf34d 100644 --- a/Sources/CommandPalette/CommandPaletteSettingsToggle.swift +++ b/Sources/CommandPalette/CommandPaletteSettingsToggle.swift @@ -457,7 +457,6 @@ enum CommandPaletteSettingsToggleCommands { ) } ), - agentSessionAutoRetryDescriptor(sectionTitle: terminal), CommandPaletteSettingToggleDescriptor( commandId: commandIdPrefix + "agentHibernation", settingsKey: "terminal.agentHibernation.enabled", diff --git a/Sources/ControlSurfaceResumeTarget.swift b/Sources/ControlSurfaceResumeTarget.swift index 54efa2be0891..a500da4e3ecb 100644 --- a/Sources/ControlSurfaceResumeTarget.swift +++ b/Sources/ControlSurfaceResumeTarget.swift @@ -54,13 +54,10 @@ enum ControlSurfaceResumeTarget { } } - func clearBinding(agentSessionEnded: Bool) { + func clearBinding() { switch self { case .workspace(_, let workspace, let surfaceID): - _ = workspace.clearSurfaceResumeBinding( - panelId: surfaceID, - agentSessionEnded: agentSessionEnded - ) + _ = workspace.clearSurfaceResumeBinding(panelId: surfaceID) case .dock(_, let dock, let surfaceID): _ = dock.clearSurfaceResumeBinding(panelId: surfaceID) } @@ -375,8 +372,7 @@ extension TerminalController { explicitTargetID: UUID?, hasResolvedWindowID: Bool, expectedCheckpointID: String?, - expectedSource: String?, - agentSessionEnded: Bool + expectedSource: String? ) -> ControlSurfaceResumeResolution { guard let tabManager = resolveTabManager(routing: routing) else { return .windowUnavailable @@ -396,7 +392,7 @@ extension TerminalController { if let expectedSource, currentBinding?.source != expectedSource { return .result(surfaceResumeSnapshot(target: target, binding: currentBinding, cleared: false)) } - target.clearBinding(agentSessionEnded: agentSessionEnded) + target.clearBinding() return .result(surfaceResumeSnapshot(target: target, binding: nil, cleared: true)) } } diff --git a/Sources/DockSplitStore+SurfaceTransfer.swift b/Sources/DockSplitStore+SurfaceTransfer.swift index 5157f37a88e0..f849449812ed 100644 --- a/Sources/DockSplitStore+SurfaceTransfer.swift +++ b/Sources/DockSplitStore+SurfaceTransfer.swift @@ -225,7 +225,6 @@ extension DockSplitStore { shellActivityState: transferredShellActivityState, restoredResumeSessionWorkingDirectory: restoredResumeSessionWorkingDirectory, resumeBinding: resumeBinding, - agentSessionRetryCompletedAttempts: preservedTransfer?.agentSessionRetryCompletedAttempts, agentRuntime: agentProvenExited ? nil : preservedTransfer?.agentRuntime, isRemoteTerminal: preservedTransfer?.isRemoteTerminal ?? false, remoteRelayPort: preservedTransfer?.remoteRelayPort, diff --git a/Sources/GhosttyApp+AgentSessionRetry.swift b/Sources/GhosttyApp+AgentSessionRetry.swift deleted file mode 100644 index 89502fe18e38..000000000000 --- a/Sources/GhosttyApp+AgentSessionRetry.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation -import GhosttyKit - -extension GhosttyApp { - func handleAgentSessionCommandFinishedAction( - tabId: UUID?, - surfaceId: UUID?, - message: ghostty_action_command_finished_s - ) -> Bool { - let exitCode = message.exit_code >= 0 ? Int(message.exit_code) : nil - guard let tabId, let surfaceId else { return true } - - Task { @MainActor in - guard let app = AppDelegate.shared, - let tabManager = app.tabManagerFor(tabId: tabId) ?? app.tabManager else { - return - } - tabManager.handleAgentSessionCommandFinished( - workspaceId: tabId, - panelId: surfaceId, - exitCode: exitCode - ) - } - return true - } -} diff --git a/Sources/GhosttySurfaceScrollView+NotificationScroll.swift b/Sources/GhosttySurfaceScrollView+NotificationScroll.swift index ca307ce7b98a..de9efe36efb9 100644 --- a/Sources/GhosttySurfaceScrollView+NotificationScroll.swift +++ b/Sources/GhosttySurfaceScrollView+NotificationScroll.swift @@ -376,7 +376,6 @@ extension GhosttySurfaceScrollView { func terminalSurfaceDidReceiveExplicitInput() { cancelPendingNotificationScrollRestoreForUserInput() - onExplicitTerminalInput?() } func restorePendingNotificationScrollPositionAfterScrollbarUpdate() { diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index e7489c3538f1..bc23c6b71b99 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -3161,14 +3161,6 @@ class GhosttyApp { ) } - if action.tag == GHOSTTY_ACTION_COMMAND_FINISHED { - return handleAgentSessionCommandFinishedAction( - tabId: callbackTabId, - surfaceId: callbackSurfaceId, - message: action.action.command_finished - ) - } - guard let surfaceView = callbackContext?.surfaceView else { return false } if action.tag == GHOSTTY_ACTION_RELOAD_CONFIG || action.tag == GHOSTTY_ACTION_CONFIG_CHANGE || @@ -8195,7 +8187,6 @@ final class GhosttySurfaceScrollView: NSView { private let flashLayer: CAShapeLayer private var cloudTerminalReconnectOverlayView: CloudTerminalReconnectOverlayView? private var hasVisibilityRevealRefreshScheduled = false - var onExplicitTerminalInput: (() -> Void)? var isRightSidebarDockSurface: Bool { surfaceView.terminalSurface?.focusPlacement == .rightSidebarDock } diff --git a/Sources/HostSettingsActions.swift b/Sources/HostSettingsActions.swift index e05e8225226a..658fbc6e8c00 100644 --- a/Sources/HostSettingsActions.swift +++ b/Sources/HostSettingsActions.swift @@ -89,14 +89,9 @@ final class HostSettingsActions: SettingsHostActions { func resetAllSettingsSideEffects() { LanguageSettingsStore(defaults: .standard).applyLanguageOverride(.system) PaneChromeSettings.notifyDidChange() - AgentSessionAutoRetrySettings().notifyDidChange() AppDelegate.shared?.reconcileSocketListenerConfiguration(source: "settings.reset_all") } - func agentSessionAutoRetrySettingDidChange() { - AgentSessionAutoRetrySettings().notifyDidChange() - } - func notifyShortcutSettingsDidChange() { // reload() already posts didChangeNotification when the file's // contents changed; posting again here double-notified every diff --git a/Sources/KeyboardShortcutSettingsFileStore+Template.swift b/Sources/KeyboardShortcutSettingsFileStore+Template.swift index 45eabf599d30..55415e2b4ca8 100644 --- a/Sources/KeyboardShortcutSettingsFileStore+Template.swift +++ b/Sources/KeyboardShortcutSettingsFileStore+Template.swift @@ -96,7 +96,6 @@ extension CmuxSettingsFileStore { "sessionContentAlignment": SessionContentAlignment.center.rawValue, "copyOnSelect": TerminalCopyOnSelectSettings.defaultCopyOnSelect, "autoResumeAgentSessions": AgentSessionAutoResumeSettings.defaultAutoResumeAgentSessions, - "autoRetryAgentSessions": AgentSessionAutoRetrySettings.defaultAutoRetryAgentSessions, "showTextBoxOnNewTerminals": TerminalTextBoxInputSettings.defaultShowOnNewTerminals, "focusTextBoxOnNewTerminals": TerminalTextBoxInputSettings.defaultFocusOnNewTerminals, "textBoxDefaultSubmitAction": TerminalTextBoxInputSettings.defaultSubmitActionID, diff --git a/Sources/KeyboardShortcutSettingsFileStore.swift b/Sources/KeyboardShortcutSettingsFileStore.swift index 18dcd1cd35be..14ac91f0a3ca 100644 --- a/Sources/KeyboardShortcutSettingsFileStore.swift +++ b/Sources/KeyboardShortcutSettingsFileStore.swift @@ -1529,7 +1529,6 @@ final class CmuxSettingsFileStore { let changes = sideEffects.changes let apply = { var agentSessionAutoResumeDidChange = false - var agentSessionAutoRetryDidChange = false var agentHibernationDidChange = false var rendererRealizationDidChange = false var paneChromeDidChange = false @@ -1550,9 +1549,6 @@ final class CmuxSettingsFileStore { if change.defaultsKey == AgentSessionAutoResumeSettings.autoResumeAgentSessionsKey { agentSessionAutoResumeDidChange = true } - if change.defaultsKey == AgentSessionAutoRetrySettings.autoRetryAgentSessionsKey { - agentSessionAutoRetryDidChange = true - } if change.defaultsKey == AgentHibernationSettings.enabledKey || change.defaultsKey == AgentHibernationSettings.idleSecondsKey || change.defaultsKey == AgentHibernationSettings.maxLiveTerminalsKey || @@ -1578,11 +1574,6 @@ final class CmuxSettingsFileStore { if agentSessionAutoResumeDidChange { AgentSessionAutoResumeSettings.notifyDidChange(notificationCenter: notificationCenter) } - if agentSessionAutoRetryDidChange { - AgentSessionAutoRetrySettings( - notificationCenter: notificationCenter - ).notifyDidChange() - } if agentHibernationDidChange { AgentHibernationSettings.notifyDidChange(notificationCenter: notificationCenter) } diff --git a/Sources/SettingsNavigation.swift b/Sources/SettingsNavigation.swift index b92c130c0414..01b48692a9fc 100644 --- a/Sources/SettingsNavigation.swift +++ b/Sources/SettingsNavigation.swift @@ -385,15 +385,6 @@ enum SettingsSearchIndex { setting(.terminal, "copy-on-select", String(localized: "settings.terminal.copyOnSelect", defaultValue: "Copy on Selection"), "terminal.copyOnSelect clipboard selection mouse double click triple click"), setting(.terminal, "tab-bar-font-size", String(localized: "settings.terminal.tabBarFontSize", defaultValue: "Tab Bar Font Size"), "font size text scale terminal browser pane tab title surface-tab-bar-font-size"), setting(.terminal, "agent-auto-resume", String(localized: "settings.terminal.agentAutoResume", defaultValue: "Resume Agent Sessions on Reopen"), "terminal.autoResumeAgentSessions auto resume restore reopen relaunch quit sessions agents claude code codex opencode rovo dev rovodev toggle"), - setting( - .terminal, - "agent-auto-retry", - String(localized: "settings.terminal.agentAutoRetry", defaultValue: "Retry Failed Agent Sessions"), - String( - localized: "settings.search.alias.setting.terminal.agent-auto-retry", - defaultValue: "terminal.autoRetryAgentSessions auto retry resume error failure transport api rate limit overloaded agents claude codex" - ) - ), setting(.terminal, "agent-hibernation", String(localized: "settings.terminal.agentHibernation", defaultValue: "Agent Hibernation"), "terminal.agentHibernation idle hibernate suspend background agents claude code codex opencode live terminals"), setting(.terminal, "renderer-realization", String(localized: "settings.terminal.rendererRealization", defaultValue: "Reclaim Offscreen Terminal Memory"), "terminal.rendererRealization renderer reclaim offscreen memory iosurface gpu idle warm release background terminals"), setting(.terminal, "resume-commands", String(localized: "settings.terminal.resumeCommands", defaultValue: "Resume Commands"), "surface resume command approvals prefixes auto restore prompt manual tmux hibernation"), @@ -570,7 +561,6 @@ enum SettingsSearchIndex { "terminal.sessionContentMaxWidth": settingID(for: .terminal, idSuffix: "session-content-width"), "terminal.sessionContentAlignment": settingID(for: .terminal, idSuffix: "session-content-alignment"), "terminal.autoResumeAgentSessions": settingID(for: .terminal, idSuffix: "agent-auto-resume"), - "terminal.autoRetryAgentSessions": settingID(for: .terminal, idSuffix: "agent-auto-retry"), "terminal.agentHibernation.enabled": settingID(for: .terminal, idSuffix: "agent-hibernation"), "terminal.agentHibernation.idleSeconds": settingID(for: .terminal, idSuffix: "agent-hibernation"), "terminal.agentHibernation.maxLiveTerminals": settingID(for: .terminal, idSuffix: "agent-hibernation"), diff --git a/Sources/TabManager+AgentSessionRetry.swift b/Sources/TabManager+AgentSessionRetry.swift deleted file mode 100644 index c9b213ca08b9..000000000000 --- a/Sources/TabManager+AgentSessionRetry.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation - -extension TabManager { - func handleAgentSessionCommandFinished( - workspaceId: UUID, - panelId: UUID, - exitCode: Int? - ) { - guard let workspace = workspacesById[workspaceId], - workspace.panels[panelId] is TerminalPanel else { - return - } - workspace.agentSessionRetryCoordinator.commandFinished( - panelId: panelId, - exitCode: exitCode - ) - } -} diff --git a/Sources/TerminalController+ControlSurfaceContext3.swift b/Sources/TerminalController+ControlSurfaceContext3.swift index f8223b95eb2b..a4c85d68cdef 100644 --- a/Sources/TerminalController+ControlSurfaceContext3.swift +++ b/Sources/TerminalController+ControlSurfaceContext3.swift @@ -8,15 +8,6 @@ import Foundation /// that file's doc comment for the overview. extension TerminalController { - func controlSurfaceResumeStrings() -> ControlSurfaceResumeStrings { - ControlSurfaceResumeStrings( - agentSessionEndedMustBeBoolean: String( - localized: "socket.surface.resume.agentSessionEndedMustBeBoolean", - defaultValue: "agent_session_ended must be a boolean" - ) - ) - } - // MARK: - move (bridge to still-app-side v2SurfaceMove) func controlSurfaceMove(params: [String: JSONValue]) -> ControlCallResult { diff --git a/Sources/Workspace+AgentLifecycle.swift b/Sources/Workspace+AgentLifecycle.swift index 90392918ae65..0526c3e73c66 100644 --- a/Sources/Workspace+AgentLifecycle.swift +++ b/Sources/Workspace+AgentLifecycle.swift @@ -223,10 +223,6 @@ extension Workspace { guard let targetPanelId, panels[targetPanelId] != nil else { return } agentLifecycleStatesByPanelId[targetPanelId, default: [:]][key] = lifecycle if !AgentHibernationLifecycleStatusKeys.isManualKey(key) { - agentSessionRetryCoordinator.agentLifecycleDidChange( - panelId: targetPanelId, - lifecycle: lifecycle - ) recordAgentLifecycleChange(panelId: targetPanelId) } } @@ -244,7 +240,6 @@ extension Workspace { } didClear = true if recordsHibernationActivity { - agentSessionRetryCoordinator.agentLifecycleDidClear(panelId: panelId) recordAgentLifecycleChange(panelId: panelId) } } @@ -259,7 +254,6 @@ extension Workspace { } func clearAgentLifecycleStates(panelId: UUID) { - agentSessionRetryCoordinator.cancel(panelId: panelId) guard let removed = agentLifecycleStatesByPanelId.removeValue(forKey: panelId) else { return } let manualStates = removed.filter { AgentHibernationLifecycleStatusKeys.isManualKey($0.key) } if !manualStates.isEmpty { @@ -281,9 +275,8 @@ extension Workspace { func clearAllAgentLifecycleStates() { let panelIds = Array(agentLifecycleStatesByPanelId.keys) - agentLifecycleStatesByPanelId.removeAll() - agentSessionRetryCoordinator.cancelAll() guard !panelIds.isEmpty else { return } + agentLifecycleStatesByPanelId.removeAll() for panelId in panelIds { recordAgentLifecycleChange(panelId: panelId) } diff --git a/Sources/Workspace+AgentSessionRetry.swift b/Sources/Workspace+AgentSessionRetry.swift deleted file mode 100644 index 23d182372503..000000000000 --- a/Sources/Workspace+AgentSessionRetry.swift +++ /dev/null @@ -1,211 +0,0 @@ -import CmuxSidebar -import Foundation - -extension Workspace { - enum ManagedAgentRetryLaunchReadiness { - case ready - case awaitingIdle - case invalidated - } - - func hasActiveAgentLifecycleForRetry(panelId: UUID) -> Bool { - (agentLifecycleStatesByPanelId[panelId] ?? [:]).contains { key, lifecycle in - !AgentHibernationLifecycleStatusKeys.isManualKey(key) && - (lifecycle == .running || lifecycle == .needsInput) - } - } - - func managedAgentRetryBinding(panelId: UUID) -> SurfaceResumeBindingSnapshot? { - guard panels[panelId] is TerminalPanel, - let binding = surfaceResumeBindingsByPanelId[panelId], - binding.isAgentHookBinding, - binding.checkpointId?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false, - binding.kind?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false else { - return nil - } - return binding - } - - /// Final MainActor ownership classification before a retry is injected. - func managedAgentRetryLaunchReadiness( - binding: SurfaceResumeBindingSnapshot, - panelId: UUID - ) -> ManagedAgentRetryLaunchReadiness { - guard panels[panelId] is TerminalPanel else { - return .invalidated - } - let currentBinding = surfaceResumeBindingsByPanelId[panelId] - guard currentBinding == nil || currentBinding == binding else { - return .invalidated - } - guard !hasActiveAgentLifecycleForRetry(panelId: panelId), - panelShellActivityStates[panelId] == .promptIdle else { - return .awaitingIdle - } - return .ready - } - - func sendManagedAgentRetry( - binding: SurfaceResumeBindingSnapshot, - panelId: UUID, - beforeSending: () -> Void = {}, - afterSending: () -> Void = {} - ) -> Bool { - let currentBinding = surfaceResumeBindingsByPanelId[panelId] - guard currentBinding == nil || currentBinding == binding, - let panel = panels[panelId] as? TerminalPanel else { - return false - } - let input: String? = switch binding.launchFlavor { - case .local: - binding.startupInputWithLauncherScript( - allowLauncherScript: true, - repairPortableAgentExecutable: true - ) - case .persistentSSH: - binding.remoteStartupInputWithLauncherScript(allowLauncherScript: false) - } - guard let input else { return false } - beforeSending() - defer { afterSending() } - return panel.sendInputResult(input).accepted - } - - func showAgentRetryScheduled( - panelId: UUID, - exitCode: Int, - attempt: Int, - maximumAttempts: Int - ) { - let status = String.localizedStringWithFormat( - String( - localized: "agent.autoRetry.status.retrying", - defaultValue: "Retrying agent (attempt %lld/%lld)…" - ), - Int64(attempt), - Int64(maximumAttempts) - ) - sidebarMetadata.addStatusEntry(SidebarStatusEntry( - key: agentRetryStatusKey(panelId: panelId), - value: status, - icon: "arrow.clockwise", - color: "#F59E0B", - priority: 200 - )) - let logMessage = String.localizedStringWithFormat( - String( - localized: "agent.autoRetry.log.scheduled", - defaultValue: "Agent exited with code %lld; retrying (attempt %lld/%lld)." - ), - Int64(exitCode), - Int64(attempt), - Int64(maximumAttempts) - ) - sidebarMetadata.appendLogEntry( - message: logMessage, - level: .warning, - source: "agent-auto-retry" - ) - } - - func showAgentRetriesExhausted( - panelId: UUID, - exitCode: Int?, - maximumAttempts: Int - ) { - sidebarMetadata.addStatusEntry(SidebarStatusEntry( - key: agentRetryStatusKey(panelId: panelId), - value: String( - localized: "agent.autoRetry.status.exhausted", - defaultValue: "Agent auto-retry exhausted" - ), - icon: "exclamationmark.arrow.triangle.2.circlepath", - color: "#EF4444", - priority: 200 - )) - let logMessage = String.localizedStringWithFormat( - String( - localized: "agent.autoRetry.log.exhausted", - defaultValue: "Agent exited with code %lld after %lld retries; automatic retries exhausted." - ), - Int64(exitCode ?? -1), - Int64(maximumAttempts) - ) - sidebarMetadata.appendLogEntry( - message: logMessage, - level: .error, - source: "agent-auto-retry" - ) - postAgentRetryNotification( - panelId: panelId, - body: agentRetryExhaustedNotificationBody(maximumAttempts: maximumAttempts) - ) - } - - private func agentRetryExhaustedNotificationBody(maximumAttempts: Int) -> String { - if maximumAttempts == 1 { - return String( - localized: "agent.autoRetry.notification.exhausted.body.one", - defaultValue: "The agent could not be resumed after 1 automatic retry." - ) - } - return String.localizedStringWithFormat( - String( - localized: "agent.autoRetry.notification.exhausted.body.other", - defaultValue: "The agent could not be resumed after %lld automatic retries." - ), - Int64(maximumAttempts) - ) - } - - func showAgentRetryLaunchFailure(panelId: UUID) { - sidebarMetadata.addStatusEntry(SidebarStatusEntry( - key: agentRetryStatusKey(panelId: panelId), - value: String( - localized: "agent.autoRetry.status.exhausted", - defaultValue: "Agent auto-retry exhausted" - ), - icon: "exclamationmark.arrow.triangle.2.circlepath", - color: "#EF4444", - priority: 200 - )) - let message = String( - localized: "agent.autoRetry.log.launchFailed", - defaultValue: "The agent resume command could not be sent; automatic retries stopped." - ) - sidebarMetadata.appendLogEntry( - message: message, - level: .error, - source: "agent-auto-retry" - ) - postAgentRetryNotification(panelId: panelId, body: message) - } - - func removeAgentRetryStatusEntry(panelId: UUID) { - statusEntries.removeValue(forKey: agentRetryStatusKey(panelId: panelId)) - } - - func removeAllAgentRetryStatusEntries() { - let prefix = "agent.auto_retry." - statusEntries = statusEntries.filter { !$0.key.hasPrefix(prefix) } - } - - private func postAgentRetryNotification(panelId: UUID, body: String) { - AppDelegate.shared?.notificationStore?.addNotification( - tabId: id, - surfaceId: panelId, - title: String( - localized: "agent.autoRetry.notification.exhausted.title", - defaultValue: "Agent retries exhausted" - ), - subtitle: title, - body: body, - cooldownKey: "agent-auto-retry-exhausted-\(id.uuidString)-\(panelId.uuidString)", - cooldownInterval: 60 - ) - } - - private func agentRetryStatusKey(panelId: UUID) -> String { - "agent.auto_retry.\(panelId.uuidString.lowercased())" - } -} diff --git a/Sources/Workspace+DetachedSurfaceTransfer.swift b/Sources/Workspace+DetachedSurfaceTransfer.swift index 9f619135069e..df7ffdde2a75 100644 --- a/Sources/Workspace+DetachedSurfaceTransfer.swift +++ b/Sources/Workspace+DetachedSurfaceTransfer.swift @@ -45,8 +45,6 @@ extension Workspace { let shellActivityState: PanelShellActivityState? let restoredResumeSessionWorkingDirectory: String? let resumeBinding: SurfaceResumeBindingSnapshot? - /// Retry attempts only when the source proved this binding owned the running command. - let agentSessionRetryCompletedAttempts: Int? let agentRuntime: DetachedAgentRuntimeState? let isRemoteTerminal: Bool let remoteRelayPort: Int? @@ -84,7 +82,6 @@ extension Workspace { shellActivityState: shellActivityState, restoredResumeSessionWorkingDirectory: restoredResumeSessionWorkingDirectory, resumeBinding: resumeBinding, - agentSessionRetryCompletedAttempts: agentSessionRetryCompletedAttempts, agentRuntime: agentRuntime, isRemoteTerminal: isRemoteTerminal, remoteRelayPort: remoteRelayPort, diff --git a/Sources/Workspace+SurfaceResumeBinding.swift b/Sources/Workspace+SurfaceResumeBinding.swift deleted file mode 100644 index f2b05b126e3d..000000000000 --- a/Sources/Workspace+SurfaceResumeBinding.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Foundation - -extension Workspace { - @discardableResult - func setSurfaceResumeBinding( - _ binding: SurfaceResumeBindingSnapshot, - panelId: UUID - ) -> Bool { - guard terminalPanel(for: panelId) != nil, - let startupInput = binding.inlineStartupInput(repairPortableAgentExecutable: false), - !startupInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { - return false - } - surfaceResumeBindingsByPanelId[panelId] = binding - agentSessionRetryCoordinator.managedResumeBindingDidChange( - panelId: panelId, - binding: binding.isAgentHookBinding ? binding : nil - ) - return true - } - - @discardableResult - func clearSurfaceResumeBinding( - panelId: UUID, - agentSessionEnded: Bool = false - ) -> Bool { - guard let binding = surfaceResumeBindingsByPanelId.removeValue(forKey: panelId) else { - return false - } - agentSessionRetryCoordinator.managedResumeBindingDidClear( - panelId: panelId, - binding: binding, - sessionDidEnd: agentSessionEnded - ) - return true - } - - func surfaceResumeBinding(panelId: UUID) -> SurfaceResumeBindingSnapshot? { - surfaceResumeBindingsByPanelId[panelId] - } -} diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 8820edf012ab..d259f5fdf480 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -2123,7 +2123,6 @@ final class Workspace: Identifiable, ObservableObject { @Published private(set) var surfaceTabBarDirectory: String? private(set) var preferredBrowserProfileID: UUID? let closeTabWarningDefaults, agentSessionAutoResumeDefaults: UserDefaults - let agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings private let settings: any SettingsReading /// Ordinal for CMUX_PORT range assignment (monotonically increasing per app session) @@ -2544,10 +2543,6 @@ final class Workspace: Identifiable, ObservableObject { var debugSessionSnapshotSyntheticScrollbackByPanelId: [UUID: String] = [:] #endif let restoredAgentLifecycle = RestoredAgentLifecycleCoordinator() - lazy var agentSessionRetryCoordinator = AgentSessionRetryCoordinator( - workspace: self, - settings: agentSessionAutoRetrySettings - ) var restoredAgentSnapshotsByPanelId: [UUID: SessionRestorableAgentSnapshot] { get { restoredAgentLifecycle.snapshotsByPanelId } set { restoredAgentLifecycle.snapshotsByPanelId = newValue } @@ -3079,7 +3074,6 @@ final class Workspace: Identifiable, ObservableObject { settings: any SettingsReading = UserDefaultsSettingsClient(defaults: .standard), closeTabWarningDefaults: UserDefaults = .standard, agentSessionAutoResumeDefaults: UserDefaults = .standard, - agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings = AgentSessionAutoRetrySettings(), initialDetachedSurface: DetachedSurfaceTransfer? = nil, sessionRestorePolicy: WorkspaceSessionRestorePolicyService? = nil, sidebarProcessTitleObservation: WorkspaceSidebarProcessTitleObservationModel? = nil, @@ -3092,7 +3086,6 @@ final class Workspace: Identifiable, ObservableObject { self.settings = settings self.closeTabWarningDefaults = closeTabWarningDefaults self.agentSessionAutoResumeDefaults = agentSessionAutoResumeDefaults - self.agentSessionAutoRetrySettings = agentSessionAutoRetrySettings let sanitizedWorkspaceEnvironment = Self.sanitizedWorkspaceEnvironment(workspaceEnvironment) self.workspaceEnvironment = sanitizedWorkspaceEnvironment self.portOrdinal = portOrdinal @@ -3792,15 +3785,6 @@ final class Workspace: Identifiable, ObservableObject { } private func configureTerminalPanel(_ terminalPanel: TerminalPanel) { - terminalPanel.hostedView.onExplicitTerminalInput = { [weak self, weak terminalPanel] in - guard let self, let terminalPanel, - self.panels[terminalPanel.id] as? TerminalPanel === terminalPanel else { - return - } - self.agentSessionRetryCoordinator.explicitTerminalInputDidBegin( - panelId: terminalPanel.id - ) - } terminalPanel.surface.onFontSizeLineageChanged = { [weak self, weak terminalPanel] lineage in guard let self, let terminalPanel, self.lastTerminalConfigInheritancePanelId == terminalPanel.id, @@ -4800,7 +4784,6 @@ final class Workspace: Identifiable, ObservableObject { return } panelShellActivityStates[panelId] = state - agentSessionRetryCoordinator.shellActivityDidChange(panelId: panelId, state: state) if let terminalPanel = panels[panelId] as? TerminalPanel { terminalPanel.updateShellActivityState(state) } @@ -4908,6 +4891,26 @@ final class Workspace: Identifiable, ObservableObject { return didResume } + @discardableResult + func setSurfaceResumeBinding(_ binding: SurfaceResumeBindingSnapshot, panelId: UUID) -> Bool { + guard terminalPanel(for: panelId) != nil, + let startupInput = binding.inlineStartupInput(repairPortableAgentExecutable: false), + !startupInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + return false + } + surfaceResumeBindingsByPanelId[panelId] = binding + return true + } + + @discardableResult + func clearSurfaceResumeBinding(panelId: UUID) -> Bool { + surfaceResumeBindingsByPanelId.removeValue(forKey: panelId) != nil + } + + func surfaceResumeBinding(panelId: UUID) -> SurfaceResumeBindingSnapshot? { + surfaceResumeBindingsByPanelId[panelId] + } + func panelNeedsConfirmClose(panelId: UUID, fallbackNeedsConfirmClose: Bool) -> Bool { Self.resolveCloseConfirmation( shellActivityState: panelShellActivityStates[panelId], @@ -9469,12 +9472,6 @@ final class Workspace: Identifiable, ObservableObject { ) } } - agentSessionRetryCoordinator.seedTransferredManagedRun( - panelId: detached.panelId, - shellActivityState: detached.shellActivityState, - binding: surfaceResumeBindingsByPanelId[detached.panelId], - completedAttempts: detached.agentSessionRetryCompletedAttempts - ) if let cleanupConfiguration = detached.remoteCleanupConfiguration { if didAdoptWorkspaceRemoteTracking { transferredRemoteCleanupConfigurationsByPanelId.removeValue(forKey: detached.panelId) @@ -12146,11 +12143,6 @@ extension Workspace: BonsplitDelegate { panelId: panelId, surfaceResumeBindingIndex: nil ) - let agentSessionRetryCompletedAttempts = agentSessionRetryCoordinator.transferredCompletedAttempts( - panelId: panelId, - shellActivityState: panelShellActivityStates[panelId], - binding: resumeBinding - ) let agentRuntime = agentRuntimeState(forPanelId: panelId) let panelDirectory = panelDirectories[panelId] splitLayout.storeDetachedTransfer(DetachedSurfaceTransfer( @@ -12181,7 +12173,6 @@ extension Workspace: BonsplitDelegate { shellActivityState: panelShellActivityStates[panelId], restoredResumeSessionWorkingDirectory: restoredResumeSessionWorkingDirectoriesByPanelId[panelId], resumeBinding: resumeBinding, - agentSessionRetryCompletedAttempts: agentSessionRetryCompletedAttempts, agentRuntime: agentRuntime, isRemoteTerminal: activeRemoteTerminalSurfaceIds.contains(panelId), remoteRelayPort: activeRemoteTerminalSurfaceIds.contains(panelId) diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index d23a0954ceb7..dc14b6b2e55f 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -131,7 +131,6 @@ 8837A0010000000000000001 /* AgentResumeReturnShellStartupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8837A0020000000000000002 /* AgentResumeReturnShellStartupTests.swift */; }; D3610B010000000000000001 /* AgentSessionAutoResumeSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3610B010000000000000002 /* AgentSessionAutoResumeSettingsTests.swift */; }; D3610B020000000000000001 /* AgentSessionAutoResumeSwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3610B020000000000000002 /* AgentSessionAutoResumeSwiftTests.swift */; }; - E9014000000000000000000B /* AgentSessionAutoRetrySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9014000000000000000000C /* AgentSessionAutoRetrySettings.swift */; }; A9E020000000000000000008 /* AgentSessionBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E010000000000000000008 /* AgentSessionBridge.swift */; }; A9F20000000000000000000B /* AgentSessionBridgeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F10000000000000000000B /* AgentSessionBridgeError.swift */; }; A9F20000000000000000000C /* AgentSessionBridgeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F10000000000000000000C /* AgentSessionBridgeRequest.swift */; }; @@ -145,13 +144,6 @@ A9E020000000000000000009 /* AgentSessionProcessStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E010000000000000000009 /* AgentSessionProcessStore.swift */; }; A9E020000000000000000001 /* AgentSessionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E010000000000000000001 /* AgentSessionProvider.swift */; }; A9E02000000000000000000F /* AgentSessionRendererKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E01000000000000000000F /* AgentSessionRendererKind.swift */; }; - E90140000000000000000019 /* AgentSessionRetryCoordinator+Scheduling.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9014000000000000000001A /* AgentSessionRetryCoordinator+Scheduling.swift */; }; - E90140000000000000000015 /* AgentSessionRetryCoordinator+SurfaceTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90140000000000000000016 /* AgentSessionRetryCoordinator+SurfaceTransfer.swift */; }; - E90140000000000000000001 /* AgentSessionRetryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90140000000000000000002 /* AgentSessionRetryCoordinator.swift */; }; - E90140000000000000000007 /* AgentSessionRetryCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90140000000000000000008 /* AgentSessionRetryCoordinatorTests.swift */; }; - E90140000000000000000013 /* AgentSessionRetryInteractionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90140000000000000000014 /* AgentSessionRetryInteractionTests.swift */; }; - E90140000000000000000017 /* AgentSessionRetryOrderingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90140000000000000000018 /* AgentSessionRetryOrderingTests.swift */; }; - E90140000000000000000011 /* AgentSessionRetryPanelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90140000000000000000012 /* AgentSessionRetryPanelState.swift */; }; A9F200000000000000000010 /* AgentSessionRunningSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F100000000000000000010 /* AgentSessionRunningSession.swift */; }; A9E030000000000000000002 /* AgentSessionSocketSurfaceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E030000000000000000001 /* AgentSessionSocketSurfaceTests.swift */; }; A9F200000000000000000011 /* AgentSessionStartedSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F100000000000000000011 /* AgentSessionStartedSession.swift */; }; @@ -819,7 +811,6 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources C0DEFF200000000000000001 /* CommandPaletteOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEFF200000000000000002 /* CommandPaletteOverlay.swift */; }; C0DE86060000000000000003 /* CommandPaletteRestoreFocusTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE86060000000000000004 /* CommandPaletteRestoreFocusTarget.swift */; }; A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008382 /* CommandPaletteSearchEngineTests.swift */; }; - E9014000000000000000000D /* CommandPaletteSettingsToggle+AgentSessionRetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9014000000000000000000E /* CommandPaletteSettingsToggle+AgentSessionRetry.swift */; }; C0DEF4110000000000000001 /* CommandPaletteSettingsToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEF4110000000000000002 /* CommandPaletteSettingsToggle.swift */; }; C0DEF4120000000000000001 /* CommandPaletteSettingsToggleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEF4120000000000000002 /* CommandPaletteSettingsToggleTests.swift */; }; C1713006C1713006C1713006 /* CommandPaletteShortcutCustomizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1713005C1713005C1713005 /* CommandPaletteShortcutCustomizationTests.swift */; }; @@ -1039,7 +1030,6 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources F0F0CF0D0000000000000001 /* ForkParentFallbackSessionIndexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F0CF0D0000000000000002 /* ForkParentFallbackSessionIndexTests.swift */; }; F17F00000000000000000003 /* FullWidthTabCommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17F00000000000000000004 /* FullWidthTabCommandTests.swift */; }; F17F00000000000000000001 /* FullWidthTabPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17F00000000000000000002 /* FullWidthTabPersistenceTests.swift */; }; - E9014000000000000000000F /* GhosttyApp+AgentSessionRetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90140000000000000000010 /* GhosttyApp+AgentSessionRetry.swift */; }; 85510001A1B2C3D4E5F60001 /* GhosttyApp+ChildExitPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85510002A1B2C3D4E5F60001 /* GhosttyApp+ChildExitPolicy.swift */; }; D0B10012A1B2C3D4E5F60001 /* GhosttyApp+SurfaceConfigurationReload.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B10013A1B2C3D4E5F60001 /* GhosttyApp+SurfaceConfigurationReload.swift */; }; EC010D01 /* GhosttyApp+TerminalCustomUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC010D02 /* GhosttyApp+TerminalCustomUpload.swift */; }; @@ -2008,7 +1998,6 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources A5F100000000000000000005 /* TabItemView+WorkspaceNotificationsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F100000000000000000006 /* TabItemView+WorkspaceNotificationsMenu.swift */; }; 186A0F2EE1C92446E0A2D1E3 /* TabItemView+WorkspaceTodo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EA28B32C38ED8C753C82D3 /* TabItemView+WorkspaceTodo.swift */; }; 804100000000000000000001 /* TabManager+AdjacentWorkspaceReordering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804100000000000000000002 /* TabManager+AdjacentWorkspaceReordering.swift */; }; - E90140000000000000000003 /* TabManager+AgentSessionRetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90140000000000000000004 /* TabManager+AgentSessionRetry.swift */; }; 7136B0017136B0017136B001 /* TabManager+BackgroundWorkspaceMountPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7136F0017136F0017136F001 /* TabManager+BackgroundWorkspaceMountPolicy.swift */; }; DCDC1000000000000000B015 /* TabManager+BrowserFocus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCDC1000000000000000B016 /* TabManager+BrowserFocus.swift */; }; D7AB00000000000000000013 /* TabManager+DetachedWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB00000000000000000014 /* TabManager+DetachedWorkspace.swift */; }; @@ -2329,7 +2318,6 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources D7632B13A1B2C3D4E5F60718 /* WKWebView+CmuxPrintOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7632B14A1B2C3D4E5F60718 /* WKWebView+CmuxPrintOperation.swift */; }; C0DE43000000000000000007 /* Workspace+AgentChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE43000000000000000008 /* Workspace+AgentChat.swift */; }; A6AC73020000000000000001 /* Workspace+AgentLifecycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6AC73020000000000000002 /* Workspace+AgentLifecycle.swift */; }; - E90140000000000000000005 /* Workspace+AgentSessionRetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90140000000000000000006 /* Workspace+AgentSessionRetry.swift */; }; 8777E0018777E0018777E001 /* Workspace+AttentionFlashRouting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8777E0028777E0028777E002 /* Workspace+AttentionFlashRouting.swift */; }; CA52B0150000000000000000 /* Workspace+CanvasLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA52C0150000000000000000 /* Workspace+CanvasLayout.swift */; }; C54860020000000000000001 /* Workspace+CmuxNavigationDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C54860020000000000000002 /* Workspace+CmuxNavigationDescriptor.swift */; }; @@ -2360,7 +2348,6 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources C51A740000000000000000C1 /* Workspace+SimulatorSessionPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = C51A740000000000000000C2 /* Workspace+SimulatorSessionPersistence.swift */; }; 8300A0040000000000000004 /* Workspace+SSHConnectionBroker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8300A0030000000000000003 /* Workspace+SSHConnectionBroker.swift */; }; CA52B0170000000000000000 /* Workspace+SurfaceNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA52C0170000000000000000 /* Workspace+SurfaceNavigation.swift */; }; - E90140000000000000000009 /* Workspace+SurfaceResumeBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9014000000000000000000A /* Workspace+SurfaceResumeBinding.swift */; }; 85911000000000000000000B /* Workspace+TerminalLinkOpening.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85911000000000000000000C /* Workspace+TerminalLinkOpening.swift */; }; 822900000000000000000001 /* Workspace+TerminalResizeInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 822900000000000000000002 /* Workspace+TerminalResizeInteraction.swift */; }; 4DF302A0C8164672B8547449 /* Workspace+TitleOwnership.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21553D9A12B4EE4974E1943 /* Workspace+TitleOwnership.swift */; }; @@ -2699,7 +2686,6 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources 8837A0020000000000000002 /* AgentResumeReturnShellStartupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentResumeReturnShellStartupTests.swift; sourceTree = ""; }; D3610B010000000000000002 /* AgentSessionAutoResumeSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentSessionAutoResumeSettingsTests.swift; sourceTree = ""; }; D3610B020000000000000002 /* AgentSessionAutoResumeSwiftTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentSessionAutoResumeSwiftTests.swift; sourceTree = ""; }; - E9014000000000000000000C /* AgentSessionAutoRetrySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/AgentSessionAutoRetrySettings.swift; sourceTree = ""; }; A9E010000000000000000008 /* AgentSessionBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/AgentSessionBridge.swift; sourceTree = ""; }; A9F10000000000000000000B /* AgentSessionBridgeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/AgentSessionBridgeError.swift; sourceTree = ""; }; A9F10000000000000000000C /* AgentSessionBridgeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/AgentSessionBridgeRequest.swift; sourceTree = ""; }; @@ -2713,13 +2699,6 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources A9E010000000000000000009 /* AgentSessionProcessStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/AgentSessionProcessStore.swift; sourceTree = ""; }; A9E010000000000000000001 /* AgentSessionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentSessionProvider.swift; sourceTree = ""; }; A9E01000000000000000000F /* AgentSessionRendererKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentSessionRendererKind.swift; sourceTree = ""; }; - E9014000000000000000001A /* AgentSessionRetryCoordinator+Scheduling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AgentSessionRetryCoordinator+Scheduling.swift"; sourceTree = ""; }; - E90140000000000000000016 /* AgentSessionRetryCoordinator+SurfaceTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AgentSessionRetryCoordinator+SurfaceTransfer.swift"; sourceTree = ""; }; - E90140000000000000000002 /* AgentSessionRetryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentSessionRetryCoordinator.swift; sourceTree = ""; }; - E90140000000000000000008 /* AgentSessionRetryCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentSessionRetryCoordinatorTests.swift; sourceTree = ""; }; - E90140000000000000000014 /* AgentSessionRetryInteractionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentSessionRetryInteractionTests.swift; sourceTree = ""; }; - E90140000000000000000018 /* AgentSessionRetryOrderingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentSessionRetryOrderingTests.swift; sourceTree = ""; }; - E90140000000000000000012 /* AgentSessionRetryPanelState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentSessionRetryPanelState.swift; sourceTree = ""; }; A9F100000000000000000010 /* AgentSessionRunningSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/AgentSessionRunningSession.swift; sourceTree = ""; }; A9E030000000000000000001 /* AgentSessionSocketSurfaceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentSessionSocketSurfaceTests.swift; sourceTree = ""; }; A9F100000000000000000011 /* AgentSessionStartedSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/AgentSessionStartedSession.swift; sourceTree = ""; }; @@ -3317,7 +3296,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = C0DEFF200000000000000002 /* CommandPaletteOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPalette/CommandPaletteOverlay.swift; sourceTree = ""; }; C0DE86060000000000000004 /* CommandPaletteRestoreFocusTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteRestoreFocusTarget.swift; sourceTree = ""; }; A5008382 /* CommandPaletteSearchEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteSearchEngineTests.swift; sourceTree = ""; }; - E9014000000000000000000E /* CommandPaletteSettingsToggle+AgentSessionRetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommandPalette/CommandPaletteSettingsToggle+AgentSessionRetry.swift"; sourceTree = ""; }; C0DEF4110000000000000002 /* CommandPaletteSettingsToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPalette/CommandPaletteSettingsToggle.swift; sourceTree = ""; }; C0DEF4120000000000000002 /* CommandPaletteSettingsToggleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteSettingsToggleTests.swift; sourceTree = ""; }; C1713005C1713005C1713005 /* CommandPaletteShortcutCustomizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteShortcutCustomizationTests.swift; sourceTree = ""; }; @@ -3537,7 +3515,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = F17F00000000000000000004 /* FullWidthTabCommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullWidthTabCommandTests.swift; sourceTree = ""; }; F17F00000000000000000002 /* FullWidthTabPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullWidthTabPersistenceTests.swift; sourceTree = ""; }; A5001017 /* ghostty.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ghostty.h; sourceTree = ""; }; - E90140000000000000000010 /* GhosttyApp+AgentSessionRetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyApp+AgentSessionRetry.swift"; sourceTree = ""; }; 85510002A1B2C3D4E5F60001 /* GhosttyApp+ChildExitPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyApp+ChildExitPolicy.swift"; sourceTree = ""; }; D0B10013A1B2C3D4E5F60001 /* GhosttyApp+SurfaceConfigurationReload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyApp+SurfaceConfigurationReload.swift"; sourceTree = ""; }; EC010D02 /* GhosttyApp+TerminalCustomUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GhosttyApp+TerminalCustomUpload.swift"; sourceTree = ""; }; @@ -4487,7 +4464,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = A5F100000000000000000006 /* TabItemView+WorkspaceNotificationsMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabItemView+WorkspaceNotificationsMenu.swift"; sourceTree = ""; }; 56EA28B32C38ED8C753C82D3 /* TabItemView+WorkspaceTodo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabItemView+WorkspaceTodo.swift"; sourceTree = ""; }; 804100000000000000000002 /* TabManager+AdjacentWorkspaceReordering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+AdjacentWorkspaceReordering.swift"; sourceTree = ""; }; - E90140000000000000000004 /* TabManager+AgentSessionRetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+AgentSessionRetry.swift"; sourceTree = ""; }; 7136F0017136F0017136F001 /* TabManager+BackgroundWorkspaceMountPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+BackgroundWorkspaceMountPolicy.swift"; sourceTree = ""; }; DCDC1000000000000000B016 /* TabManager+BrowserFocus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+BrowserFocus.swift"; sourceTree = ""; }; D7AB00000000000000000014 /* TabManager+DetachedWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+DetachedWorkspace.swift"; sourceTree = ""; }; @@ -4808,7 +4784,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = D7632B14A1B2C3D4E5F60718 /* WKWebView+CmuxPrintOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Panels/WKWebView+CmuxPrintOperation.swift"; sourceTree = ""; }; C0DE43000000000000000008 /* Workspace+AgentChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+AgentChat.swift"; sourceTree = ""; }; A6AC73020000000000000002 /* Workspace+AgentLifecycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+AgentLifecycle.swift"; sourceTree = ""; }; - E90140000000000000000006 /* Workspace+AgentSessionRetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+AgentSessionRetry.swift"; sourceTree = ""; }; 8777E0028777E0028777E002 /* Workspace+AttentionFlashRouting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+AttentionFlashRouting.swift"; sourceTree = ""; }; CA52C0150000000000000000 /* Workspace+CanvasLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+CanvasLayout.swift"; sourceTree = ""; }; C54860020000000000000002 /* Workspace+CmuxNavigationDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+CmuxNavigationDescriptor.swift"; sourceTree = ""; }; @@ -4839,7 +4814,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = C51A740000000000000000C2 /* Workspace+SimulatorSessionPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+SimulatorSessionPersistence.swift"; sourceTree = ""; }; 8300A0030000000000000003 /* Workspace+SSHConnectionBroker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+SSHConnectionBroker.swift"; sourceTree = ""; }; CA52C0170000000000000000 /* Workspace+SurfaceNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+SurfaceNavigation.swift"; sourceTree = ""; }; - E9014000000000000000000A /* Workspace+SurfaceResumeBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+SurfaceResumeBinding.swift"; sourceTree = ""; }; 85911000000000000000000C /* Workspace+TerminalLinkOpening.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+TerminalLinkOpening.swift"; sourceTree = ""; }; 822900000000000000000002 /* Workspace+TerminalResizeInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+TerminalResizeInteraction.swift"; sourceTree = ""; }; D21553D9A12B4EE4974E1943 /* Workspace+TitleOwnership.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+TitleOwnership.swift"; sourceTree = ""; }; @@ -5459,7 +5433,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = C0DE32470000000000000002 /* ContentViewIdentifierCopyCommands.swift */, C0DE1A020000000000000002 /* SavedLayoutStore.swift */, 9960CC3992F3C44489E7627C /* WorkspaceRuntimeSettings.swift */, - E9014000000000000000000C /* AgentSessionAutoRetrySettings.swift */, D5671002D5671002D5671002 /* TerminalScrollSpeedSettings.swift */, F65760100000000000000002 /* AgentHibernationController+Confirmation.swift */, 8997D00E8997D00E8997D00E /* AgentHibernationController+CommittedTerminationCleanup.swift */, @@ -5624,7 +5597,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = 5B0C00010000000000000002 /* SidebarResizerOcclusionResolver.swift */, C0DEFF200000000000000002 /* CommandPaletteOverlay.swift */, C0DEF4110000000000000002 /* CommandPaletteSettingsToggle.swift */, - E9014000000000000000000E /* CommandPaletteSettingsToggle+AgentSessionRetry.swift */, 9DAB808A8EC74C40B95F7672 /* GhosttySurfaceConfigurationRefresh.swift */, 6B8E2E03F4A64C61B729CF19 /* TerminalDirectoryOpenSupport.swift */, 8A4FE96C3F394FC6A6D4B018 /* CmuxCLIPathInstaller.swift */, @@ -5724,7 +5696,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = C06552010000000000000004 /* NotificationBurstCoalescer.swift */, C06552010000000000000002 /* PanelTitleUpdateCoalescingSettings.swift */, A5001013 /* TabManager.swift */, - E90140000000000000000004 /* TabManager+AgentSessionRetry.swift */, 7136F0017136F0017136F001 /* TabManager+BackgroundWorkspaceMountPolicy.swift */, C0DE1A030000000000000002 /* TabManager+SavedLayouts.swift */, C548600B0000000000000002 /* TabManager+SessionRestoreIdentity.swift */, @@ -5765,10 +5736,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = A5001511 /* UITestRecorder.swift */, A5001520 /* PostHogAnalytics.swift */, A5001416 /* Workspace.swift */, - E90140000000000000000002 /* AgentSessionRetryCoordinator.swift */, - E9014000000000000000001A /* AgentSessionRetryCoordinator+Scheduling.swift */, - E90140000000000000000016 /* AgentSessionRetryCoordinator+SurfaceTransfer.swift */, - E90140000000000000000012 /* AgentSessionRetryPanelState.swift */, 8777E0028777E0028777E002 /* Workspace+AttentionFlashRouting.swift */, 791100020000000000000001 /* Workspace+RemoteDisconnectPlaceholder.swift */, 797800030000000000000002 /* Workspace+PersistentRemotePTYReattach.swift */, @@ -5779,8 +5746,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = 8300A0030000000000000003 /* Workspace+SSHConnectionBroker.swift */, 791102020000000000000001 /* RemoteDisconnectPreparationService.swift */, C0DE43000000000000000008 /* Workspace+AgentChat.swift */, - E90140000000000000000006 /* Workspace+AgentSessionRetry.swift */, - E9014000000000000000000A /* Workspace+SurfaceResumeBinding.swift */, C3744E040000000000000001 /* AgentPIDProcessIdentity.swift */, 6799A0026799A0026799A002 /* WorkspaceSidebarAgentRuntimeObservationModel.swift */, 4AF464BD9CEDF14931D55126 /* Workspace+Todos.swift */, @@ -5911,7 +5876,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = 85911000000000000000000C /* Workspace+TerminalLinkOpening.swift */, C40410010000000000000020 /* FileExplorerDoubleClickActionSettings.swift */, C0DEF828300000000000002 /* GhosttyTerminalImmediateHostedStateAction.swift */, - E90140000000000000000010 /* GhosttyApp+AgentSessionRetry.swift */, E89950010000000000000002 /* GhosttyFlashOverlayView.swift */, A5001015 /* GhosttyTerminalView.swift */, E89950040000000000000002 /* KeyboardCopyModeGridMetrics.swift */, @@ -7030,9 +6994,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = 842300000000000000000006 /* RestorableAgentProcessGenerationTests.swift */, 842300000000000000000008 /* SurfaceResumeAgentBindingGenerationTests.swift */, D3610B020000000000000002 /* AgentSessionAutoResumeSwiftTests.swift */, - E90140000000000000000008 /* AgentSessionRetryCoordinatorTests.swift */, - E90140000000000000000014 /* AgentSessionRetryInteractionTests.swift */, - E90140000000000000000018 /* AgentSessionRetryOrderingTests.swift */, A9E010000000000000000005 /* AgentExecutableResolverTests.swift */, A9E030000000000000000001 /* AgentSessionSocketSurfaceTests.swift */, C51A720000000000000000B2 /* SimulatorPanelIntegrationTests.swift */, @@ -8172,7 +8133,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = 0A1107110000000000000014 /* AgentRestoreMode.swift in Sources */, D8DFFAEC2DBC3ACCF87D563E /* AgentResumeLaunchGuard.swift in Sources */, CC2D14DFE13EFF544A25BEAF /* AgentResumeLiveness.swift in Sources */, - E9014000000000000000000B /* AgentSessionAutoRetrySettings.swift in Sources */, A9E020000000000000000008 /* AgentSessionBridge.swift in Sources */, A9F20000000000000000000B /* AgentSessionBridgeError.swift in Sources */, A9F20000000000000000000C /* AgentSessionBridgeRequest.swift in Sources */, @@ -8186,10 +8146,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = A9E020000000000000000009 /* AgentSessionProcessStore.swift in Sources */, A9E020000000000000000001 /* AgentSessionProvider.swift in Sources */, A9E02000000000000000000F /* AgentSessionRendererKind.swift in Sources */, - E90140000000000000000019 /* AgentSessionRetryCoordinator+Scheduling.swift in Sources */, - E90140000000000000000015 /* AgentSessionRetryCoordinator+SurfaceTransfer.swift in Sources */, - E90140000000000000000001 /* AgentSessionRetryCoordinator.swift in Sources */, - E90140000000000000000011 /* AgentSessionRetryPanelState.swift in Sources */, A9F200000000000000000010 /* AgentSessionRunningSession.swift in Sources */, A9F200000000000000000011 /* AgentSessionStartedSession.swift in Sources */, A9E02000000000000000000A /* AgentSessionStreamAccumulators.swift in Sources */, @@ -8505,7 +8461,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = C0DE86060000000000000001 /* CommandPaletteFocusRestoreCoordinator.swift in Sources */, C0DEFF200000000000000001 /* CommandPaletteOverlay.swift in Sources */, C0DE86060000000000000003 /* CommandPaletteRestoreFocusTarget.swift in Sources */, - E9014000000000000000000D /* CommandPaletteSettingsToggle+AgentSessionRetry.swift in Sources */, C0DEF4110000000000000001 /* CommandPaletteSettingsToggle.swift in Sources */, C1713002C1713002C1713002 /* CommandPaletteShortcutRouting.swift in Sources */, 3023A1013023A1013023A101 /* ConfigSettingsView.swift in Sources */, @@ -8663,7 +8618,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = 5E55400000000000000000D1 /* FocusStealingResponderConformances.swift in Sources */, C0DEFB100000000000000001 /* FocusSurfaceBroadcaster.swift in Sources */, AA5269A0C0DE0002FACE0002 /* ForeignFirstResponderPolicy.swift in Sources */, - E9014000000000000000000F /* GhosttyApp+AgentSessionRetry.swift in Sources */, 85510001A1B2C3D4E5F60001 /* GhosttyApp+ChildExitPolicy.swift in Sources */, D0B10012A1B2C3D4E5F60001 /* GhosttyApp+SurfaceConfigurationReload.swift in Sources */, EC010D01 /* GhosttyApp+TerminalCustomUpload.swift in Sources */, @@ -9303,7 +9257,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = A5F100000000000000000005 /* TabItemView+WorkspaceNotificationsMenu.swift in Sources */, 186A0F2EE1C92446E0A2D1E3 /* TabItemView+WorkspaceTodo.swift in Sources */, 804100000000000000000001 /* TabManager+AdjacentWorkspaceReordering.swift in Sources */, - E90140000000000000000003 /* TabManager+AgentSessionRetry.swift in Sources */, 7136B0017136B0017136B001 /* TabManager+BackgroundWorkspaceMountPolicy.swift in Sources */, DCDC1000000000000000B015 /* TabManager+BrowserFocus.swift in Sources */, D7AB00000000000000000013 /* TabManager+DetachedWorkspace.swift in Sources */, @@ -9561,7 +9514,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = D7632B13A1B2C3D4E5F60718 /* WKWebView+CmuxPrintOperation.swift in Sources */, C0DE43000000000000000007 /* Workspace+AgentChat.swift in Sources */, A6AC73020000000000000001 /* Workspace+AgentLifecycle.swift in Sources */, - E90140000000000000000005 /* Workspace+AgentSessionRetry.swift in Sources */, 8777E0018777E0018777E001 /* Workspace+AttentionFlashRouting.swift in Sources */, CA52B0150000000000000000 /* Workspace+CanvasLayout.swift in Sources */, C54860020000000000000001 /* Workspace+CmuxNavigationDescriptor.swift in Sources */, @@ -9592,7 +9544,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = C51A740000000000000000C1 /* Workspace+SimulatorSessionPersistence.swift in Sources */, 8300A0040000000000000004 /* Workspace+SSHConnectionBroker.swift in Sources */, CA52B0170000000000000000 /* Workspace+SurfaceNavigation.swift in Sources */, - E90140000000000000000009 /* Workspace+SurfaceResumeBinding.swift in Sources */, 85911000000000000000000B /* Workspace+TerminalLinkOpening.swift in Sources */, 822900000000000000000001 /* Workspace+TerminalResizeInteraction.swift in Sources */, 4DF302A0C8164672B8547449 /* Workspace+TitleOwnership.swift in Sources */, @@ -9902,9 +9853,6 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa = 8837A0010000000000000001 /* AgentResumeReturnShellStartupTests.swift in Sources */, D3610B010000000000000001 /* AgentSessionAutoResumeSettingsTests.swift in Sources */, D3610B020000000000000001 /* AgentSessionAutoResumeSwiftTests.swift in Sources */, - E90140000000000000000007 /* AgentSessionRetryCoordinatorTests.swift in Sources */, - E90140000000000000000013 /* AgentSessionRetryInteractionTests.swift in Sources */, - E90140000000000000000017 /* AgentSessionRetryOrderingTests.swift in Sources */, A9E030000000000000000002 /* AgentSessionSocketSurfaceTests.swift in Sources */, A9E050000000000000000002 /* AgentSessionWebRendererTests.swift in Sources */, 725746692D9647948561044D /* AppDelegateBareSpaceShortcutRoutingTests.swift in Sources */, diff --git a/cmuxTests/AgentSessionRetryCoordinatorTests.swift b/cmuxTests/AgentSessionRetryCoordinatorTests.swift deleted file mode 100644 index dd4667210545..000000000000 --- a/cmuxTests/AgentSessionRetryCoordinatorTests.swift +++ /dev/null @@ -1,514 +0,0 @@ -import Foundation -import Testing - -#if canImport(cmux_DEV) -@testable import cmux_DEV -#elseif canImport(cmux) -@testable import cmux -#endif - -@Suite("Agent session retry coordinator", .serialized) -struct AgentSessionRetryCoordinatorTests { - @MainActor - @Test("managed session teardown and idle retain the retry candidate until exit classification") - func managedSessionTeardownRetainsCandidate() throws { - let defaults = try #require(UserDefaults(suiteName: "AgentSessionRetryCoordinatorTests.retained")) - defer { defaults.removePersistentDomain(forName: "AgentSessionRetryCoordinatorTests.retained") } - defaults.set(true, forKey: AgentSessionAutoRetrySettings.autoRetryAgentSessionsKey) - - let workspace = Workspace( - agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings(defaults: defaults) - ) - let panelId = try #require(workspace.focusedPanelId) - let binding = managedBinding(sessionId: "retry-after-teardown") - workspace.updatePanelShellActivityState(panelId: panelId, state: .commandRunning) - #expect(workspace.setSurfaceResumeBinding(binding, panelId: panelId)) - #expect(workspace.managedAgentRetryBinding(panelId: panelId) == binding) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .running) - - #expect(workspace.clearSurfaceResumeBinding(panelId: panelId, agentSessionEnded: true)) - #expect(workspace.surfaceResumeBinding(panelId: panelId) == nil) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .idle) - #expect(workspace.clearAgentLifecycle(key: "claude_code", panelId: panelId)) - #expect(!workspace.hasActiveAgentLifecycleForRetry(panelId: panelId)) - workspace.updatePanelShellActivityState(panelId: panelId, state: .promptIdle) - workspace.agentSessionRetryCoordinator.commandFinished(panelId: panelId, exitCode: 1) - - let retryStatusKey = "agent.auto_retry.\(panelId.uuidString.lowercased())" - let status = try #require(workspace.statusEntries[retryStatusKey]) - let expectedStatus = String.localizedStringWithFormat( - String( - localized: "agent.autoRetry.status.retrying", - defaultValue: "Retrying agent (attempt %lld/%lld)…" - ), - Int64(1), - Int64(3) - ) - #expect(status.key == retryStatusKey) - #expect(status.value == expectedStatus) - #expect(status.icon == "arrow.clockwise") - #expect(status.priority == 200) - #expect(workspace.statusEntries.keys.filter { $0.hasPrefix("agent.auto_retry.") } == [retryStatusKey]) - workspace.agentSessionRetryCoordinator.cancelAll() - } - - @MainActor - @Test("classified failure remains scheduled across late lifecycle teardown") - func classifiedFailureSurvivesLateLifecycleTeardown() throws { - let suiteName = "AgentSessionRetryCoordinatorTests.lateTeardown" - let defaults = try #require(UserDefaults(suiteName: suiteName)) - defer { defaults.removePersistentDomain(forName: suiteName) } - defaults.set(true, forKey: AgentSessionAutoRetrySettings.autoRetryAgentSessionsKey) - - let workspace = Workspace( - agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings(defaults: defaults) - ) - let panelId = try #require(workspace.focusedPanelId) - let binding = managedBinding(sessionId: "retry-before-late-teardown") - workspace.updatePanelShellActivityState(panelId: panelId, state: .commandRunning) - #expect(workspace.setSurfaceResumeBinding(binding, panelId: panelId)) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .running) - - #expect(workspace.clearSurfaceResumeBinding(panelId: panelId, agentSessionEnded: true)) - workspace.agentSessionRetryCoordinator.commandFinished(panelId: panelId, exitCode: 1) - let retryStatusKey = "agent.auto_retry.\(panelId.uuidString.lowercased())" - #expect(workspace.statusEntries[retryStatusKey]?.icon == "arrow.clockwise") - - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .idle) - #expect(workspace.clearAgentLifecycle(key: "claude_code", panelId: panelId)) - - #expect(workspace.statusEntries[retryStatusKey]?.icon == "arrow.clockwise") - workspace.agentSessionRetryCoordinator.cancelAll() - } - - @MainActor - @Test("a new shell command cancels a scheduled retry") - func newShellCommandCancelsScheduledRetry() throws { - let suiteName = "AgentSessionRetryCoordinatorTests.newCommandAfterFailure" - let defaults = try #require(UserDefaults(suiteName: suiteName)) - defer { defaults.removePersistentDomain(forName: suiteName) } - defaults.set(true, forKey: AgentSessionAutoRetrySettings.autoRetryAgentSessionsKey) - - let workspace = Workspace( - agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings(defaults: defaults) - ) - let panelId = try #require(workspace.focusedPanelId) - let binding = managedBinding(sessionId: "retry-cancelled-by-new-command") - workspace.updatePanelShellActivityState(panelId: panelId, state: .commandRunning) - #expect(workspace.setSurfaceResumeBinding(binding, panelId: panelId)) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .running) - - #expect(workspace.clearSurfaceResumeBinding(panelId: panelId, agentSessionEnded: true)) - workspace.agentSessionRetryCoordinator.commandFinished(panelId: panelId, exitCode: 1) - let retryStatusKey = "agent.auto_retry.\(panelId.uuidString.lowercased())" - #expect(workspace.statusEntries[retryStatusKey]?.icon == "arrow.clockwise") - - workspace.updatePanelShellActivityState(panelId: panelId, state: .promptIdle) - workspace.updatePanelShellActivityState(panelId: panelId, state: .commandRunning) - - #expect(workspace.statusEntries[retryStatusKey] == nil) - } - - @MainActor - @Test("ordinary resume binding clears fail closed") - func ordinaryBindingClearCancelsCandidate() throws { - let defaults = try #require(UserDefaults(suiteName: "AgentSessionRetryCoordinatorTests.cleared")) - defer { defaults.removePersistentDomain(forName: "AgentSessionRetryCoordinatorTests.cleared") } - defaults.set(true, forKey: AgentSessionAutoRetrySettings.autoRetryAgentSessionsKey) - - let workspace = Workspace( - agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings(defaults: defaults) - ) - let panelId = try #require(workspace.focusedPanelId) - workspace.updatePanelShellActivityState(panelId: panelId, state: .commandRunning) - #expect(workspace.setSurfaceResumeBinding( - managedBinding(sessionId: "manual-clear"), - panelId: panelId - )) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .running) - - #expect(workspace.clearSurfaceResumeBinding(panelId: panelId)) - workspace.agentSessionRetryCoordinator.commandFinished(panelId: panelId, exitCode: 1) - - #expect(!workspace.statusEntries.keys.contains { - $0.hasPrefix("agent.auto_retry.") - }) - } - - @MainActor - @Test("a later shell command invalidates an unclassified ended session") - func laterCommandInvalidatesEndedSession() throws { - let defaults = try #require(UserDefaults(suiteName: "AgentSessionRetryCoordinatorTests.laterCommand")) - defer { defaults.removePersistentDomain(forName: "AgentSessionRetryCoordinatorTests.laterCommand") } - defaults.set(true, forKey: AgentSessionAutoRetrySettings.autoRetryAgentSessionsKey) - - let workspace = Workspace( - agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings(defaults: defaults) - ) - let panelId = try #require(workspace.focusedPanelId) - let binding = managedBinding(sessionId: "ended-before-later-command") - workspace.updatePanelShellActivityState(panelId: panelId, state: .commandRunning) - #expect(workspace.setSurfaceResumeBinding(binding, panelId: panelId)) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .running) - - #expect(workspace.clearSurfaceResumeBinding(panelId: panelId, agentSessionEnded: true)) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .idle) - #expect(workspace.clearAgentLifecycle(key: "claude_code", panelId: panelId)) - workspace.updatePanelShellActivityState(panelId: panelId, state: .promptIdle) - workspace.updatePanelShellActivityState(panelId: panelId, state: .commandRunning) - - workspace.agentSessionRetryCoordinator.commandFinished(panelId: panelId, exitCode: 1) - - #expect(!workspace.statusEntries.keys.contains { - $0.hasPrefix("agent.auto_retry.") - }) - } - - @MainActor - @Test("a replayed running state does not invalidate the command awaiting classification") - func duplicateRunningStateRetainsEndedSession() throws { - let suiteName = "AgentSessionRetryCoordinatorTests.duplicateRunning" - let defaults = try #require(UserDefaults(suiteName: suiteName)) - defer { defaults.removePersistentDomain(forName: suiteName) } - defaults.set(true, forKey: AgentSessionAutoRetrySettings.autoRetryAgentSessionsKey) - - let workspace = Workspace( - agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings(defaults: defaults) - ) - let panelId = try #require(workspace.focusedPanelId) - let binding = managedBinding(sessionId: "ended-before-running-replay") - workspace.updatePanelShellActivityState(panelId: panelId, state: .commandRunning) - #expect(workspace.setSurfaceResumeBinding(binding, panelId: panelId)) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .running) - - #expect(workspace.clearSurfaceResumeBinding(panelId: panelId, agentSessionEnded: true)) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .running) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .idle) - #expect(workspace.clearAgentLifecycle(key: "claude_code", panelId: panelId)) - workspace.updatePanelShellActivityState(panelId: panelId, state: .promptIdle) - workspace.agentSessionRetryCoordinator.commandFinished(panelId: panelId, exitCode: 1) - - let retryStatusKey = "agent.auto_retry.\(panelId.uuidString.lowercased())" - #expect(workspace.statusEntries[retryStatusKey]?.icon == "arrow.clockwise") - workspace.agentSessionRetryCoordinator.cancelAll() - } - - @MainActor - @Test("active retry phases refresh matching bindings and reject replacement sessions") - func activeRetryPhasesValidateManagedSessionIdentity() throws { - let phases: [(name: String, phase: AgentSessionRetryPanelState.Phase)] = [ - ("awaitingLaunch", .awaitingLaunch(attempt: 1, maximumAttempts: 3)), - ("running", .running(attempt: 1, maximumAttempts: 3)), - ] - - for phaseCase in phases { - let matchingWorkspace = Workspace() - let matchingPanelId = try #require(matchingWorkspace.focusedPanelId) - let originalBinding = managedBinding(sessionId: "\(phaseCase.name)-same") - let refreshedBinding = managedBinding( - sessionId: "\(phaseCase.name)-same", - command: "claude --resume \(phaseCase.name)-same --updated", - updatedAt: 1_888_888_888 - ) - let matchingCoordinator = matchingWorkspace.agentSessionRetryCoordinator - matchingCoordinator.statesByPanelId[matchingPanelId] = .init( - completedAttempts: 1, - binding: originalBinding, - commandGeneration: 7, - phase: phaseCase.phase, - timer: nil - ) - - #expect(matchingWorkspace.setSurfaceResumeBinding( - refreshedBinding, - panelId: matchingPanelId - )) - let refreshedState = try #require( - matchingCoordinator.statesByPanelId[matchingPanelId], - "Matching \(phaseCase.name) binding must preserve recovery" - ) - #expect(refreshedState.binding == refreshedBinding) - #expect( - refreshedState.phase == .running(attempt: 1, maximumAttempts: 3), - "Matching \(phaseCase.name) binding must acknowledge or preserve active recovery" - ) - - let replacementWorkspace = Workspace() - let replacementPanelId = try #require(replacementWorkspace.focusedPanelId) - let replacementCoordinator = replacementWorkspace.agentSessionRetryCoordinator - replacementCoordinator.statesByPanelId[replacementPanelId] = .init( - completedAttempts: 1, - binding: originalBinding, - commandGeneration: 7, - phase: phaseCase.phase, - timer: nil - ) - let replacementBinding = managedBinding(sessionId: "\(phaseCase.name)-replacement") - - #expect(replacementWorkspace.setSurfaceResumeBinding( - replacementBinding, - panelId: replacementPanelId - )) - #expect( - replacementCoordinator.statesByPanelId[replacementPanelId] == nil, - "Mismatched \(phaseCase.name) binding must fail closed before retry acknowledgement" - ) - #expect(replacementCoordinator.managedRunsByPanelId[replacementPanelId] == nil) - } - } - - @MainActor - @Test("a delayed command completion cannot retry a replacement session") - func replacementSessionInvalidatesEndedCandidate() throws { - let suiteName = "AgentSessionRetryCoordinatorTests.replacementSession" - let defaults = try #require(UserDefaults(suiteName: suiteName)) - defer { defaults.removePersistentDomain(forName: suiteName) } - defaults.set(true, forKey: AgentSessionAutoRetrySettings.autoRetryAgentSessionsKey) - - let workspace = Workspace( - agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings(defaults: defaults) - ) - let panelId = try #require(workspace.focusedPanelId) - let endedBinding = managedBinding(sessionId: "ended-session") - let replacementBinding = managedBinding(sessionId: "replacement-session") - workspace.updatePanelShellActivityState(panelId: panelId, state: .commandRunning) - #expect(workspace.setSurfaceResumeBinding(endedBinding, panelId: panelId)) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .running) - #expect(workspace.clearSurfaceResumeBinding(panelId: panelId, agentSessionEnded: true)) - - #expect(workspace.setSurfaceResumeBinding(replacementBinding, panelId: panelId)) - workspace.agentSessionRetryCoordinator.commandFinished(panelId: panelId, exitCode: 1) - - #expect(workspace.surfaceResumeBinding(panelId: panelId) == replacementBinding) - #expect(workspace.statusEntries[retryStatusKey(panelId: panelId)] == nil) - } - - @MainActor - @Test("a binding observed outside the current command cannot authorize retry") - func bindingWithoutCommandOwnershipFailsClosed() throws { - let suiteName = "AgentSessionRetryCoordinatorTests.noEndedCandidate" - let defaults = try #require(UserDefaults(suiteName: suiteName)) - defer { defaults.removePersistentDomain(forName: suiteName) } - defaults.set(true, forKey: AgentSessionAutoRetrySettings.autoRetryAgentSessionsKey) - - let workspace = Workspace( - agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings(defaults: defaults) - ) - let panelId = try #require(workspace.focusedPanelId) - let currentBinding = managedBinding(sessionId: "still-current") - #expect(workspace.setSurfaceResumeBinding(currentBinding, panelId: panelId)) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .running) - - workspace.agentSessionRetryCoordinator.commandFinished(panelId: panelId, exitCode: 1) - - #expect(workspace.surfaceResumeBinding(panelId: panelId) == currentBinding) - #expect(workspace.statusEntries[retryStatusKey(panelId: panelId)] == nil) - } - - @MainActor - @Test("a nonzero managed agent exit retries without a SessionEnd hook") - func nonzeroManagedExitWithoutSessionEndRetries() throws { - let suiteName = "AgentSessionRetryCoordinatorTests.hardExit" - let defaults = try #require(UserDefaults(suiteName: suiteName)) - defer { defaults.removePersistentDomain(forName: suiteName) } - defaults.set(true, forKey: AgentSessionAutoRetrySettings.autoRetryAgentSessionsKey) - - let workspace = Workspace( - agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings(defaults: defaults) - ) - let panelId = try #require(workspace.focusedPanelId) - let binding = managedBinding(sessionId: "hard-nonzero-exit") - workspace.updatePanelShellActivityState(panelId: panelId, state: .commandRunning) - #expect(workspace.setSurfaceResumeBinding(binding, panelId: panelId)) - workspace.setAgentLifecycle(key: "codex", panelId: panelId, lifecycle: .running) - workspace.setAgentLifecycle(key: "codex", panelId: panelId, lifecycle: .idle) - workspace.updatePanelShellActivityState(panelId: panelId, state: .promptIdle) - - workspace.agentSessionRetryCoordinator.commandFinished(panelId: panelId, exitCode: 1) - - #expect(workspace.statusEntries[retryStatusKey(panelId: panelId)]?.icon == "arrow.clockwise") - #expect(workspace.clearSurfaceResumeBinding(panelId: panelId, agentSessionEnded: true)) - #expect(workspace.statusEntries[retryStatusKey(panelId: panelId)]?.icon == "arrow.clockwise") - workspace.agentSessionRetryCoordinator.cancelAll() - } - - @MainActor - @Test("an intentional signal exit never retries") - func signalExitDoesNotRetry() throws { - let suiteName = "AgentSessionRetryCoordinatorTests.signalExit" - let defaults = try #require(UserDefaults(suiteName: suiteName)) - defer { defaults.removePersistentDomain(forName: suiteName) } - defaults.set(true, forKey: AgentSessionAutoRetrySettings.autoRetryAgentSessionsKey) - - let workspace = Workspace( - agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings(defaults: defaults) - ) - let panelId = try #require(workspace.focusedPanelId) - workspace.updatePanelShellActivityState(panelId: panelId, state: .commandRunning) - #expect(workspace.setSurfaceResumeBinding( - managedBinding(sessionId: "ctrl-c-exit"), - panelId: panelId - )) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .running) - workspace.updatePanelShellActivityState(panelId: panelId, state: .promptIdle) - - workspace.agentSessionRetryCoordinator.commandFinished(panelId: panelId, exitCode: 130) - - #expect(workspace.statusEntries[retryStatusKey(panelId: panelId)] == nil) - } - - @MainActor - @Test("timer fire invalidates replacement ownership but waits for delayed idle signals") - func timerFireRevalidatesPaneOwnership() throws { - let activeLifecycle = try scheduledRetry( - suiteName: "AgentSessionRetryCoordinatorTests.timerActiveLifecycle", - sessionId: "active-lifecycle" - ) - defer { - activeLifecycle.defaults.removePersistentDomain( - forName: "AgentSessionRetryCoordinatorTests.timerActiveLifecycle" - ) - } - activeLifecycle.workspace.agentLifecycleStatesByPanelId[activeLifecycle.panelId] = [ - "claude_code": .running, - ] - activeLifecycle.workspace.agentSessionRetryCoordinator.retryTimerFired( - panelId: activeLifecycle.panelId - ) - #expect( - activeLifecycle.workspace.statusEntries[activeLifecycle.statusKey]?.icon == - "arrow.clockwise" - ) - - let replacementBinding = try scheduledRetry( - suiteName: "AgentSessionRetryCoordinatorTests.timerReplacementBinding", - sessionId: "binding-owner" - ) - defer { - replacementBinding.defaults.removePersistentDomain( - forName: "AgentSessionRetryCoordinatorTests.timerReplacementBinding" - ) - } - replacementBinding.workspace.surfaceResumeBindingsByPanelId[replacementBinding.panelId] = - managedBinding(sessionId: "new-binding-owner") - replacementBinding.workspace.agentSessionRetryCoordinator.retryTimerFired( - panelId: replacementBinding.panelId - ) - #expect(replacementBinding.workspace.statusEntries[replacementBinding.statusKey] == nil) - - let runningShell = try scheduledRetry( - suiteName: "AgentSessionRetryCoordinatorTests.timerRunningShell", - sessionId: "running-shell" - ) - defer { - runningShell.defaults.removePersistentDomain( - forName: "AgentSessionRetryCoordinatorTests.timerRunningShell" - ) - } - runningShell.workspace.panelShellActivityStates[runningShell.panelId] = .commandRunning - runningShell.workspace.agentSessionRetryCoordinator.retryTimerFired( - panelId: runningShell.panelId - ) - #expect(runningShell.workspace.statusEntries[runningShell.statusKey]?.icon == "arrow.clockwise") - - let unknownShell = try scheduledRetry( - suiteName: "AgentSessionRetryCoordinatorTests.timerUnknownShell", - sessionId: "unknown-shell" - ) - defer { - unknownShell.defaults.removePersistentDomain( - forName: "AgentSessionRetryCoordinatorTests.timerUnknownShell" - ) - } - unknownShell.workspace.panelShellActivityStates[unknownShell.panelId] = .unknown - unknownShell.workspace.agentSessionRetryCoordinator.retryTimerFired( - panelId: unknownShell.panelId - ) - #expect(unknownShell.workspace.statusEntries[unknownShell.statusKey]?.icon == "arrow.clockwise") - - activeLifecycle.workspace.agentSessionRetryCoordinator.cancelAll() - replacementBinding.workspace.agentSessionRetryCoordinator.cancelAll() - runningShell.workspace.agentSessionRetryCoordinator.cancelAll() - unknownShell.workspace.agentSessionRetryCoordinator.cancelAll() - } - - @MainActor - @Test("disabling auto-retry immediately cancels scheduled recovery") - func disablingCancelsScheduledRecovery() throws { - let suiteName = "AgentSessionRetryCoordinatorTests.settingDisabled" - let notificationCenter = NotificationCenter() - let fixture = try scheduledRetry( - suiteName: suiteName, - sessionId: "disabled-while-waiting", - notificationCenter: notificationCenter - ) - defer { fixture.defaults.removePersistentDomain(forName: suiteName) } - let settings = AgentSessionAutoRetrySettings( - defaults: fixture.defaults, - notificationCenter: notificationCenter - ) - - settings.setEnabled(false) - - #expect(fixture.workspace.statusEntries[fixture.statusKey] == nil) - settings.setEnabled(true) - fixture.workspace.agentSessionRetryCoordinator.retryTimerFired(panelId: fixture.panelId) - #expect(fixture.workspace.statusEntries[fixture.statusKey] == nil) - } - - @MainActor - private func scheduledRetry( - suiteName: String, - sessionId: String, - notificationCenter: NotificationCenter = .default - ) throws -> ( - workspace: Workspace, - defaults: UserDefaults, - panelId: UUID, - statusKey: String - ) { - let defaults = try #require(UserDefaults(suiteName: suiteName)) - defaults.removePersistentDomain(forName: suiteName) - let settings = AgentSessionAutoRetrySettings( - defaults: defaults, - notificationCenter: notificationCenter - ) - settings.setEnabled(true) - let workspace = Workspace(agentSessionAutoRetrySettings: settings) - let panelId = try #require(workspace.focusedPanelId) - workspace.updatePanelShellActivityState(panelId: panelId, state: .commandRunning) - #expect(workspace.setSurfaceResumeBinding( - managedBinding(sessionId: sessionId), - panelId: panelId - )) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .running) - #expect(workspace.clearSurfaceResumeBinding(panelId: panelId, agentSessionEnded: true)) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .idle) - #expect(workspace.clearAgentLifecycle(key: "claude_code", panelId: panelId)) - workspace.updatePanelShellActivityState(panelId: panelId, state: .promptIdle) - workspace.agentSessionRetryCoordinator.commandFinished(panelId: panelId, exitCode: 1) - let statusKey = retryStatusKey(panelId: panelId) - #expect(workspace.statusEntries[statusKey]?.icon == "arrow.clockwise") - return (workspace, defaults, panelId, statusKey) - } - - private func retryStatusKey(panelId: UUID) -> String { - "agent.auto_retry.\(panelId.uuidString.lowercased())" - } - - private func managedBinding( - sessionId: String, - command: String? = nil, - updatedAt: TimeInterval = 1_777_777_777 - ) -> SurfaceResumeBindingSnapshot { - SurfaceResumeBindingSnapshot( - name: "Claude", - kind: "claude", - command: command ?? "claude --resume \(sessionId)", - checkpointId: sessionId, - source: "agent-hook", - autoResume: true, - updatedAt: updatedAt - ) - } -} diff --git a/cmuxTests/AgentSessionRetryInteractionTests.swift b/cmuxTests/AgentSessionRetryInteractionTests.swift deleted file mode 100644 index 12f98a498563..000000000000 --- a/cmuxTests/AgentSessionRetryInteractionTests.swift +++ /dev/null @@ -1,366 +0,0 @@ -import Foundation -import Testing - -#if canImport(cmux_DEV) -@testable import cmux_DEV -#elseif canImport(cmux) -@testable import cmux -#endif - -@Suite("Agent session retry interactions", .serialized) -struct AgentSessionRetryInteractionTests { - @MainActor - @Test("explicit terminal input cancels a retry before it can share the prompt") - func explicitInputCancelsScheduledRetry() throws { - let fixture = try scheduledRetry(suiteName: "AgentSessionRetryInteractionTests.input") - defer { fixture.defaults.removePersistentDomain(forName: fixture.suiteName) } - let panel = try #require(fixture.workspace.panels[fixture.panelId] as? TerminalPanel) - - panel.surface.didReceiveExplicitInput() - - #expect(fixture.workspace.statusEntries[fixture.statusKey] == nil) - fixture.workspace.agentSessionRetryCoordinator.retryTimerFired(panelId: fixture.panelId) - #expect(fixture.workspace.statusEntries[fixture.statusKey] == nil) - } - - @MainActor - @Test("explicit input at an ended prompt invalidates unclassified retry ownership") - func explicitInputInvalidatesEndedRunBeforeClassification() throws { - let suiteName = "AgentSessionRetryInteractionTests.unclassifiedInput" - let defaults = try #require(UserDefaults(suiteName: suiteName)) - defer { defaults.removePersistentDomain(forName: suiteName) } - defaults.set(true, forKey: AgentSessionAutoRetrySettings.autoRetryAgentSessionsKey) - let workspace = Workspace( - agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings(defaults: defaults) - ) - let panelId = try #require(workspace.focusedPanelId) - let panel = try #require(workspace.panels[panelId] as? TerminalPanel) - workspace.updatePanelShellActivityState(panelId: panelId, state: .commandRunning) - #expect(workspace.setSurfaceResumeBinding( - managedBinding(sessionId: "ended-before-input"), - panelId: panelId - )) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .running) - #expect(workspace.clearSurfaceResumeBinding(panelId: panelId, agentSessionEnded: true)) - workspace.updatePanelShellActivityState(panelId: panelId, state: .promptIdle) - - panel.surface.didReceiveExplicitInput() - workspace.agentSessionRetryCoordinator.commandFinished(panelId: panelId, exitCode: 1) - - #expect(workspace.statusEntries[retryStatusKey(panelId: panelId)] == nil) - } - - @MainActor - @Test("moving an active managed run preserves retry ownership") - func activeRunTransferPreservesRetryOwnership() throws { - let suiteName = "AgentSessionRetryInteractionTests.transfer" - let defaults = try #require(UserDefaults(suiteName: suiteName)) - defer { defaults.removePersistentDomain(forName: suiteName) } - defaults.set(true, forKey: AgentSessionAutoRetrySettings.autoRetryAgentSessionsKey) - let source = Workspace( - agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings(defaults: defaults) - ) - let destination = Workspace( - agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings(defaults: defaults) - ) - let panelId = try #require(source.focusedPanelId) - let binding = managedBinding(sessionId: "moved-active-run") - source.updatePanelShellActivityState(panelId: panelId, state: .commandRunning) - #expect(source.setSurfaceResumeBinding(binding, panelId: panelId)) - source.setAgentLifecycle(key: "codex", panelId: panelId, lifecycle: .running) - - let transfer = try #require(source.detachSurface(panelId: panelId)) - #expect(transfer.agentSessionRetryCompletedAttempts == 0) - let destinationPaneId = try #require(destination.bonsplitController.allPaneIds.first) - #expect(destination.attachDetachedSurface( - transfer, - inPane: destinationPaneId, - focus: false - ) == panelId) - - destination.updatePanelShellActivityState(panelId: panelId, state: .promptIdle) - destination.agentSessionRetryCoordinator.commandFinished(panelId: panelId, exitCode: 1) - - #expect(destination.statusEntries[retryStatusKey(panelId: panelId)]?.icon == "arrow.clockwise") - destination.agentSessionRetryCoordinator.cancelAll() - } - - @MainActor - @Test("a retry command start owns its generation before fresh hooks arrive") - func retryStartBeforeHooksRetainsOwnership() throws { - let fixture = try scheduledRetry( - suiteName: "AgentSessionRetryInteractionTests.startBeforeHooks" - ) - defer { fixture.defaults.removePersistentDomain(forName: fixture.suiteName) } - - fixture.workspace.agentSessionRetryCoordinator.retryTimerFired( - panelId: fixture.panelId - ) - #expect( - fixture.workspace.agentSessionRetryCoordinator.statesByPanelId[fixture.panelId]?.phase == - .awaitingLaunch(attempt: 1, maximumAttempts: 3) - ) - - fixture.workspace.updatePanelShellActivityState( - panelId: fixture.panelId, - state: .commandRunning - ) - #expect( - fixture.workspace.agentSessionRetryCoordinator.statesByPanelId[fixture.panelId]?.phase == - .running(attempt: 1, maximumAttempts: 3) - ) - #expect(fixture.workspace.statusEntries[fixture.statusKey] == nil) - - fixture.workspace.updatePanelShellActivityState( - panelId: fixture.panelId, - state: .promptIdle - ) - fixture.workspace.agentSessionRetryCoordinator.commandFinished( - panelId: fixture.panelId, - exitCode: 1 - ) - - let expectedStatus = String.localizedStringWithFormat( - String( - localized: "agent.autoRetry.status.retrying", - defaultValue: "Retrying agent (attempt %lld/%lld)…" - ), - Int64(2), - Int64(3) - ) - #expect( - fixture.workspace.agentSessionRetryCoordinator.statesByPanelId[fixture.panelId]?.phase == - .waiting(attempt: 2, maximumAttempts: 3, exitCode: 1) - ) - #expect(fixture.workspace.statusEntries[fixture.statusKey]?.value == expectedStatus) - fixture.workspace.agentSessionRetryCoordinator.cancelAll() - } - - @MainActor - @Test("a retry failure before start hooks advances the bounded retry budget") - func retryFailureBeforeStartHooksAdvancesBudget() throws { - let fixture = try scheduledRetry( - suiteName: "AgentSessionRetryInteractionTests.failureBeforeStartHooks" - ) - defer { fixture.defaults.removePersistentDomain(forName: fixture.suiteName) } - - fixture.workspace.agentSessionRetryCoordinator.retryTimerFired( - panelId: fixture.panelId - ) - #expect( - fixture.workspace.agentSessionRetryCoordinator.statesByPanelId[fixture.panelId]?.phase == - .awaitingLaunch(attempt: 1, maximumAttempts: 3) - ) - fixture.workspace.setAgentLifecycle( - key: "claude_code", - panelId: fixture.panelId, - lifecycle: .idle - ) - - fixture.workspace.agentSessionRetryCoordinator.commandFinished( - panelId: fixture.panelId, - exitCode: 1 - ) - - let expectedStatus = String.localizedStringWithFormat( - String( - localized: "agent.autoRetry.status.retrying", - defaultValue: "Retrying agent (attempt %lld/%lld)…" - ), - Int64(2), - Int64(3) - ) - #expect( - fixture.workspace.agentSessionRetryCoordinator.statesByPanelId[fixture.panelId]?.phase == - .waiting(attempt: 2, maximumAttempts: 3, exitCode: 1) - ) - #expect(fixture.workspace.statusEntries[fixture.statusKey]?.value == expectedStatus) - fixture.workspace.agentSessionRetryCoordinator.cancelAll() - } - - @MainActor - @Test("an unacknowledged retry launch expires and cannot claim a later command") - func unacknowledgedLaunchExpires() throws { - let fixture = try scheduledRetry( - suiteName: "AgentSessionRetryInteractionTests.launchDeadline" - ) - defer { fixture.defaults.removePersistentDomain(forName: fixture.suiteName) } - fixture.workspace.agentSessionRetryCoordinator.retryTimerFired( - panelId: fixture.panelId - ) - #expect( - fixture.workspace.agentSessionRetryCoordinator.statesByPanelId[fixture.panelId]?.phase == - .awaitingLaunch(attempt: 1, maximumAttempts: 3) - ) - - fixture.workspace.agentSessionRetryCoordinator.retryLaunchAcknowledgementDeadlineFired( - panelId: fixture.panelId - ) - fixture.workspace.updatePanelShellActivityState( - panelId: fixture.panelId, - state: .commandRunning - ) - fixture.workspace.updatePanelShellActivityState( - panelId: fixture.panelId, - state: .promptIdle - ) - fixture.workspace.agentSessionRetryCoordinator.commandFinished( - panelId: fixture.panelId, - exitCode: 1 - ) - - #expect(fixture.workspace.agentSessionRetryCoordinator.statesByPanelId[fixture.panelId] == nil) - #expect(fixture.workspace.statusEntries[fixture.statusKey] == nil) - } - - @MainActor - @Test("explicit input cancels an accepted retry before launch acknowledgement") - func explicitInputCancelsAwaitingLaunch() throws { - let fixture = try scheduledRetry( - suiteName: "AgentSessionRetryInteractionTests.awaitingInput" - ) - defer { fixture.defaults.removePersistentDomain(forName: fixture.suiteName) } - let panel = try #require(fixture.workspace.panels[fixture.panelId] as? TerminalPanel) - fixture.workspace.agentSessionRetryCoordinator.retryTimerFired(panelId: fixture.panelId) - #expect( - fixture.workspace.agentSessionRetryCoordinator.statesByPanelId[fixture.panelId]?.phase == - .awaitingLaunch(attempt: 1, maximumAttempts: 3) - ) - - panel.surface.didReceiveExplicitInput() - - #expect(fixture.workspace.agentSessionRetryCoordinator.statesByPanelId[fixture.panelId] == nil) - #expect(fixture.workspace.statusEntries[fixture.statusKey] == nil) - } - - @MainActor - @Test("same-session hooks acknowledge a retry and clear its retrying status") - func sameSessionHooksAcknowledgeLaunch() throws { - let fixture = try scheduledRetry( - suiteName: "AgentSessionRetryInteractionTests.hookAck" - ) - defer { fixture.defaults.removePersistentDomain(forName: fixture.suiteName) } - let binding = managedBinding(sessionId: "input-cancelled-retry") - fixture.workspace.agentSessionRetryCoordinator.retryTimerFired(panelId: fixture.panelId) - - #expect(fixture.workspace.setSurfaceResumeBinding(binding, panelId: fixture.panelId)) - #expect( - fixture.workspace.agentSessionRetryCoordinator.statesByPanelId[fixture.panelId]?.phase == - .running(attempt: 1, maximumAttempts: 3) - ) - fixture.workspace.setAgentLifecycle( - key: "claude_code", - panelId: fixture.panelId, - lifecycle: .unknown - ) - - #expect( - fixture.workspace.agentSessionRetryCoordinator.statesByPanelId[fixture.panelId]?.phase == - .running(attempt: 1, maximumAttempts: 3) - ) - #expect(fixture.workspace.statusEntries[fixture.statusKey] == nil) - - fixture.workspace.updatePanelShellActivityState( - panelId: fixture.panelId, - state: .commandRunning - ) - #expect(fixture.workspace.clearSurfaceResumeBinding( - panelId: fixture.panelId, - agentSessionEnded: true - )) - fixture.workspace.updatePanelShellActivityState( - panelId: fixture.panelId, - state: .promptIdle - ) - fixture.workspace.setAgentLifecycle( - key: "claude_code", - panelId: fixture.panelId, - lifecycle: .idle - ) - #expect(fixture.workspace.clearAgentLifecycle( - key: "claude_code", - panelId: fixture.panelId - )) - fixture.workspace.agentSessionRetryCoordinator.commandFinished( - panelId: fixture.panelId, - exitCode: 0 - ) - - #expect(fixture.workspace.agentSessionRetryCoordinator.statesByPanelId[fixture.panelId] == nil) - #expect(fixture.workspace.statusEntries[fixture.statusKey] == nil) - } - - @MainActor - @Test("prompt-idle binding cleanup preserves the ended command for classification") - func bindingOnlyCleanupPreservesEndedCandidate() throws { - let suiteName = "AgentSessionRetryInteractionTests.bindingCleanup" - let defaults = try #require(UserDefaults(suiteName: suiteName)) - defer { defaults.removePersistentDomain(forName: suiteName) } - defaults.set(true, forKey: AgentSessionAutoRetrySettings.autoRetryAgentSessionsKey) - let workspace = Workspace( - agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings(defaults: defaults) - ) - let panelId = try #require(workspace.focusedPanelId) - let binding = managedBinding(sessionId: "binding-only-cleanup") - workspace.updatePanelShellActivityState(panelId: panelId, state: .commandRunning) - #expect(workspace.setSurfaceResumeBinding(binding, panelId: panelId)) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .running) - workspace.restoredAgentResumeStatesByPanelId[panelId] = .observedAgentCommandRunning - - workspace.updatePanelShellActivityState(panelId: panelId, state: .promptIdle) - #expect(workspace.surfaceResumeBinding(panelId: panelId) == nil) - workspace.agentSessionRetryCoordinator.commandFinished(panelId: panelId, exitCode: 1) - - #expect(workspace.statusEntries[retryStatusKey(panelId: panelId)]?.icon == "arrow.clockwise") - workspace.agentSessionRetryCoordinator.cancelAll() - } - - @MainActor - private func scheduledRetry( - suiteName: String - ) throws -> ( - workspace: Workspace, - defaults: UserDefaults, - panelId: UUID, - statusKey: String, - suiteName: String - ) { - let defaults = try #require(UserDefaults(suiteName: suiteName)) - defaults.removePersistentDomain(forName: suiteName) - defaults.set(true, forKey: AgentSessionAutoRetrySettings.autoRetryAgentSessionsKey) - let workspace = Workspace( - agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings(defaults: defaults) - ) - let panelId = try #require(workspace.focusedPanelId) - workspace.updatePanelShellActivityState(panelId: panelId, state: .commandRunning) - #expect(workspace.setSurfaceResumeBinding( - managedBinding(sessionId: "input-cancelled-retry"), - panelId: panelId - )) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .running) - #expect(workspace.clearSurfaceResumeBinding(panelId: panelId, agentSessionEnded: true)) - workspace.setAgentLifecycle(key: "claude_code", panelId: panelId, lifecycle: .idle) - #expect(workspace.clearAgentLifecycle(key: "claude_code", panelId: panelId)) - workspace.updatePanelShellActivityState(panelId: panelId, state: .promptIdle) - workspace.agentSessionRetryCoordinator.commandFinished(panelId: panelId, exitCode: 1) - let statusKey = retryStatusKey(panelId: panelId) - #expect(workspace.statusEntries[statusKey]?.icon == "arrow.clockwise") - return (workspace, defaults, panelId, statusKey, suiteName) - } - - private func retryStatusKey(panelId: UUID) -> String { - "agent.auto_retry.\(panelId.uuidString.lowercased())" - } - - private func managedBinding(sessionId: String) -> SurfaceResumeBindingSnapshot { - SurfaceResumeBindingSnapshot( - name: "Claude", - kind: "claude", - command: "claude --resume \(sessionId)", - checkpointId: sessionId, - source: "agent-hook", - autoResume: true, - updatedAt: 1_777_777_777 - ) - } -} diff --git a/cmuxTests/AgentSessionRetryOrderingTests.swift b/cmuxTests/AgentSessionRetryOrderingTests.swift deleted file mode 100644 index 5d7b03efcb6f..000000000000 --- a/cmuxTests/AgentSessionRetryOrderingTests.swift +++ /dev/null @@ -1,307 +0,0 @@ -import Foundation -import Testing - -#if canImport(cmux_DEV) -@testable import cmux_DEV -#elseif canImport(cmux) -@testable import cmux -#endif - -@Suite("Agent session retry event ordering", .serialized) -struct AgentSessionRetryOrderingTests { - enum HookPublicationOrder: CaseIterable, Sendable, CustomTestStringConvertible { - case bindingThenLifecycle - case lifecycleThenBinding - - var testDescription: String { - switch self { - case .bindingThenLifecycle: - "binding then lifecycle" - case .lifecycleThenBinding: - "lifecycle then binding" - } - } - } - - @MainActor - @Test( - "hook-first managed starts retain ownership when the shell generation arrives", - arguments: HookPublicationOrder.allCases - ) - func hookFirstStartRetainsOwnership(order: HookPublicationOrder) throws { - let fixture = try makeFixture(suffix: order.testDescription) - defer { fixture.defaults.removePersistentDomain(forName: fixture.suiteName) } - let binding = managedBinding(sessionId: "hook-first-\(order.testDescription)") - - fixture.workspace.updatePanelShellActivityState( - panelId: fixture.panelId, - state: .promptIdle - ) - publishHooks( - order: order, - binding: binding, - workspace: fixture.workspace, - panelId: fixture.panelId - ) - fixture.workspace.updatePanelShellActivityState( - panelId: fixture.panelId, - state: .commandRunning - ) - - endManagedRun( - binding: binding, - workspace: fixture.workspace, - panelId: fixture.panelId - ) - fixture.workspace.agentSessionRetryCoordinator.commandFinished( - panelId: fixture.panelId, - exitCode: 1 - ) - - #expect(fixture.workspace.statusEntries[fixture.statusKey]?.icon == "arrow.clockwise") - fixture.workspace.agentSessionRetryCoordinator.cancelAll() - } - - @MainActor - @Test("explicit input cancels a hook-first start before its shell generation") - func explicitInputCancelsHookFirstStart() throws { - let fixture = try makeFixture(suffix: "explicit-input") - defer { fixture.defaults.removePersistentDomain(forName: fixture.suiteName) } - let binding = managedBinding(sessionId: "hook-first-explicit-input") - let panel = try #require(fixture.workspace.panels[fixture.panelId] as? TerminalPanel) - - fixture.workspace.updatePanelShellActivityState( - panelId: fixture.panelId, - state: .promptIdle - ) - publishHooks( - order: .bindingThenLifecycle, - binding: binding, - workspace: fixture.workspace, - panelId: fixture.panelId - ) - panel.surface.didReceiveExplicitInput() - fixture.workspace.updatePanelShellActivityState( - panelId: fixture.panelId, - state: .commandRunning - ) - - endManagedRun( - binding: binding, - workspace: fixture.workspace, - panelId: fixture.panelId - ) - fixture.workspace.agentSessionRetryCoordinator.commandFinished( - panelId: fixture.panelId, - exitCode: 1 - ) - - #expect(fixture.workspace.statusEntries[fixture.statusKey] == nil) - } - - @MainActor - @Test("backoff expiry waits for authoritative teardown and preserves the retry budget") - func backoffExpiryWaitsForTeardown() throws { - let fixture = try makeFixture(suffix: "late-teardown") - defer { fixture.defaults.removePersistentDomain(forName: fixture.suiteName) } - let binding = managedBinding(sessionId: "late-teardown") - - fixture.workspace.updatePanelShellActivityState( - panelId: fixture.panelId, - state: .commandRunning - ) - #expect(fixture.workspace.setSurfaceResumeBinding(binding, panelId: fixture.panelId)) - fixture.workspace.setAgentLifecycle( - key: "claude_code", - panelId: fixture.panelId, - lifecycle: .running - ) - #expect(fixture.workspace.clearSurfaceResumeBinding( - panelId: fixture.panelId, - agentSessionEnded: true - )) - fixture.workspace.agentSessionRetryCoordinator.commandFinished( - panelId: fixture.panelId, - exitCode: 1 - ) - - fixture.workspace.agentSessionRetryCoordinator.retryTimerFired(panelId: fixture.panelId) - #expect(fixture.workspace.statusEntries[fixture.statusKey]?.icon == "arrow.clockwise") - #expect( - fixture.workspace.agentSessionRetryCoordinator.statesByPanelId[fixture.panelId]?.phase == - .ready(attempt: 1, maximumAttempts: 3) - ) - - finishLifecycleTeardown(workspace: fixture.workspace, panelId: fixture.panelId) - fixture.workspace.updatePanelShellActivityState( - panelId: fixture.panelId, - state: .promptIdle - ) - #expect( - fixture.workspace.agentSessionRetryCoordinator.statesByPanelId[fixture.panelId]?.phase == - .awaitingLaunch(attempt: 1, maximumAttempts: 3) - ) - - fixture.workspace.updatePanelShellActivityState( - panelId: fixture.panelId, - state: .commandRunning - ) - #expect(fixture.workspace.setSurfaceResumeBinding(binding, panelId: fixture.panelId)) - fixture.workspace.setAgentLifecycle( - key: "claude_code", - panelId: fixture.panelId, - lifecycle: .running - ) - endManagedRun( - binding: binding, - workspace: fixture.workspace, - panelId: fixture.panelId - ) - fixture.workspace.agentSessionRetryCoordinator.commandFinished( - panelId: fixture.panelId, - exitCode: 1 - ) - - let expectedStatus = String.localizedStringWithFormat( - String( - localized: "agent.autoRetry.status.retrying", - defaultValue: "Retrying agent (attempt %lld/%lld)…" - ), - Int64(2), - Int64(3) - ) - #expect(fixture.workspace.statusEntries[fixture.statusKey]?.value == expectedStatus) - fixture.workspace.agentSessionRetryCoordinator.cancelAll() - } - - @MainActor - @Test("retry readiness deadline abandons a pane that never becomes idle") - func readinessDeadlineAbandonsRetry() throws { - let fixture = try makeFixture(suffix: "readiness-deadline") - defer { fixture.defaults.removePersistentDomain(forName: fixture.suiteName) } - let binding = managedBinding(sessionId: "readiness-deadline") - - fixture.workspace.updatePanelShellActivityState( - panelId: fixture.panelId, - state: .commandRunning - ) - #expect(fixture.workspace.setSurfaceResumeBinding(binding, panelId: fixture.panelId)) - fixture.workspace.setAgentLifecycle( - key: "claude_code", - panelId: fixture.panelId, - lifecycle: .running - ) - #expect(fixture.workspace.clearSurfaceResumeBinding( - panelId: fixture.panelId, - agentSessionEnded: true - )) - fixture.workspace.agentSessionRetryCoordinator.commandFinished( - panelId: fixture.panelId, - exitCode: 1 - ) - fixture.workspace.agentSessionRetryCoordinator.retryTimerFired(panelId: fixture.panelId) - #expect( - fixture.workspace.agentSessionRetryCoordinator.statesByPanelId[fixture.panelId]?.phase == - .ready(attempt: 1, maximumAttempts: 3) - ) - - fixture.workspace.agentSessionRetryCoordinator.retryReadinessDeadlineFired( - panelId: fixture.panelId - ) - - #expect( - fixture.workspace.agentSessionRetryCoordinator.statesByPanelId[fixture.panelId] == nil - ) - #expect(fixture.workspace.statusEntries[fixture.statusKey] == nil) - } - - @MainActor - private func publishHooks( - order: HookPublicationOrder, - binding: SurfaceResumeBindingSnapshot, - workspace: Workspace, - panelId: UUID - ) { - switch order { - case .bindingThenLifecycle: - #expect(workspace.setSurfaceResumeBinding(binding, panelId: panelId)) - workspace.setAgentLifecycle( - key: "claude_code", - panelId: panelId, - lifecycle: .running - ) - case .lifecycleThenBinding: - workspace.setAgentLifecycle( - key: "claude_code", - panelId: panelId, - lifecycle: .running - ) - #expect(workspace.setSurfaceResumeBinding(binding, panelId: panelId)) - } - } - - @MainActor - private func endManagedRun( - binding: SurfaceResumeBindingSnapshot, - workspace: Workspace, - panelId: UUID - ) { - #expect(workspace.surfaceResumeBinding(panelId: panelId) == binding) - #expect(workspace.clearSurfaceResumeBinding( - panelId: panelId, - agentSessionEnded: true - )) - finishLifecycleTeardown(workspace: workspace, panelId: panelId) - workspace.updatePanelShellActivityState(panelId: panelId, state: .promptIdle) - } - - @MainActor - private func finishLifecycleTeardown(workspace: Workspace, panelId: UUID) { - workspace.setAgentLifecycle( - key: "claude_code", - panelId: panelId, - lifecycle: .idle - ) - #expect(workspace.clearAgentLifecycle(key: "claude_code", panelId: panelId)) - } - - @MainActor - private func makeFixture( - suffix: String - ) throws -> ( - workspace: Workspace, - defaults: UserDefaults, - panelId: UUID, - statusKey: String, - suiteName: String - ) { - let normalizedSuffix = suffix.replacingOccurrences(of: " ", with: "-") - let suiteName = "AgentSessionRetryOrderingTests.\(normalizedSuffix)" - let defaults = try #require(UserDefaults(suiteName: suiteName)) - defaults.removePersistentDomain(forName: suiteName) - defaults.set(true, forKey: AgentSessionAutoRetrySettings.autoRetryAgentSessionsKey) - let workspace = Workspace( - agentSessionAutoRetrySettings: AgentSessionAutoRetrySettings(defaults: defaults) - ) - let panelId = try #require(workspace.focusedPanelId) - return ( - workspace, - defaults, - panelId, - "agent.auto_retry.\(panelId.uuidString.lowercased())", - suiteName - ) - } - - private func managedBinding(sessionId: String) -> SurfaceResumeBindingSnapshot { - SurfaceResumeBindingSnapshot( - name: "Claude", - kind: "claude", - command: "claude --resume \(sessionId)", - checkpointId: sessionId, - source: "agent-hook", - autoResume: true, - updatedAt: 1_777_777_777 - ) - } -} diff --git a/cmuxTests/AppDelegateEqualizeSplitsShortcutTests.swift b/cmuxTests/AppDelegateEqualizeSplitsShortcutTests.swift index 3eef18d97cac..a497d0561ebd 100644 --- a/cmuxTests/AppDelegateEqualizeSplitsShortcutTests.swift +++ b/cmuxTests/AppDelegateEqualizeSplitsShortcutTests.swift @@ -7892,7 +7892,6 @@ final class AppDelegateEqualizeSplitsShortcutTests { shellActivityState: nil, restoredResumeSessionWorkingDirectory: nil, resumeBinding: nil, - agentSessionRetryCompletedAttempts: nil, agentRuntime: nil, isRemoteTerminal: false, remoteRelayPort: nil, diff --git a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift index 2966cbe99523..115f9b1bbc9e 100644 --- a/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift +++ b/cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift @@ -1692,52 +1692,6 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase { XCTAssertEqual(request["surface_id"] as? String, context.surfaceId) XCTAssertEqual(request["checkpoint_id"] as? String, sessionId) XCTAssertEqual(request["source"] as? String, "agent-hook") - XCTAssertEqual(request["agent_session_ended"] as? Bool, true) - } - - func testClaudeSessionEndWithoutAuthoritativeCheckpointDoesNotMarkBindingEnded() throws { - let context = try makeClaudeHookContext(name: "claude-session-end-missing-checkpoint") - defer { context.cleanup() } - - let reportedSessionId = "reported-session" - let now = Date().timeIntervalSince1970 - let store: [String: Any] = [ - "version": 1, - "sessions": [ - reportedSessionId: [ - "sessionId": " ", - "workspaceId": context.workspaceId, - "surfaceId": context.surfaceId, - "cwd": context.root.path, - "startedAt": now, - "updatedAt": now, - ], - ], - ] - let stateURL = context.root.appendingPathComponent("claude-hook-sessions.json") - try JSONSerialization.data(withJSONObject: store, options: [.prettyPrinted]) - .write(to: stateURL, options: .atomic) - - let result = runClaudeHook( - context: context, - arguments: ["hooks", "claude", "session-end"], - standardInput: #"{"session_id":"\#(reportedSessionId)","cwd":"\#(context.root.path)","hook_event_name":"SessionEnd"}"# - ) - - XCTAssertFalse(result.timedOut, result.stderr) - XCTAssertEqual(result.status, 0, result.stderr) - let clearRequests = context.state.commands.compactMap { command -> [String: Any]? in - guard let payload = jsonObject(command), - payload["method"] as? String == "surface.resume.clear" else { - return nil - } - return payload["params"] as? [String: Any] - } - let request = try XCTUnwrap(clearRequests.first) - XCTAssertEqual(request["surface_id"] as? String, context.surfaceId) - XCTAssertEqual(request["source"] as? String, "agent-hook") - XCTAssertNil(request["checkpoint_id"]) - XCTAssertNil(request["agent_session_ended"]) } func testNestedCodexPromptAndStopDoNotReplaceParentResumeBinding() throws { @@ -8153,7 +8107,6 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase { XCTAssertEqual(request["surface_id"] as? String, surfaceId) XCTAssertEqual(request["checkpoint_id"] as? String, sessionId) XCTAssertEqual(request["source"] as? String, "agent-hook") - XCTAssertEqual(request["agent_session_ended"] as? Bool, true) } func testSurfaceResumeClearCLIForwardsCheckpointAndSourceGuards() throws { @@ -8213,7 +8166,6 @@ final class CLINotifyProcessIntegrationRegressionTests: XCTestCase { XCTAssertEqual(request["surface_id"] as? String, surfaceId) XCTAssertEqual(request["checkpoint_id"] as? String, "new-session") XCTAssertEqual(request["source"] as? String, "agent-hook") - XCTAssertNil(request["agent_session_ended"]) } func testSurfaceResumeSetCLIPreservesQuotedShellCommand() throws { diff --git a/cmuxTests/DockPortalReconcileTests.swift b/cmuxTests/DockPortalReconcileTests.swift index f16496b787ef..84c23993ecee 100644 --- a/cmuxTests/DockPortalReconcileTests.swift +++ b/cmuxTests/DockPortalReconcileTests.swift @@ -677,7 +677,6 @@ struct DockPortalReconcileTests { shellActivityState: nil, restoredResumeSessionWorkingDirectory: nil, resumeBinding: nil, - agentSessionRetryCompletedAttempts: nil, agentRuntime: nil, isRemoteTerminal: false, remoteRelayPort: nil, diff --git a/cmuxTests/DockTerminalReattachTests.swift b/cmuxTests/DockTerminalReattachTests.swift index 04a90eee85e9..652b9074746e 100644 --- a/cmuxTests/DockTerminalReattachTests.swift +++ b/cmuxTests/DockTerminalReattachTests.swift @@ -82,7 +82,6 @@ extension DockSocketLifecycleTests { shellActivityState: shellActivityState, restoredResumeSessionWorkingDirectory: restoredResumeSessionWorkingDirectory, resumeBinding: resumeBinding, - agentSessionRetryCompletedAttempts: nil, agentRuntime: agentRuntime, isRemoteTerminal: false, remoteRelayPort: nil, diff --git a/cmuxTests/DockWorkingDirectoryInheritanceTests.swift b/cmuxTests/DockWorkingDirectoryInheritanceTests.swift index 94b582f2c333..32c00a3768c0 100644 --- a/cmuxTests/DockWorkingDirectoryInheritanceTests.swift +++ b/cmuxTests/DockWorkingDirectoryInheritanceTests.swift @@ -392,7 +392,6 @@ struct DockWorkingDirectoryInheritanceTests { shellActivityState: nil, restoredResumeSessionWorkingDirectory: nil, resumeBinding: nil, - agentSessionRetryCompletedAttempts: nil, agentRuntime: nil, isRemoteTerminal: true, remoteRelayPort: nil, diff --git a/cmuxTests/SettingsSearchIndexTests.swift b/cmuxTests/SettingsSearchIndexTests.swift index 59bc690a2948..244a7007d41e 100644 --- a/cmuxTests/SettingsSearchIndexTests.swift +++ b/cmuxTests/SettingsSearchIndexTests.swift @@ -73,13 +73,6 @@ struct SettingsSearchIndexTests { ) } - @Test func settingsPathAnchorIncludesAgentAutoRetry() { - #expect( - SettingsSearchIndex.anchorID(forSettingsPath: "terminal.autoRetryAgentSessions") - == SettingsSearchIndex.settingID(for: .terminal, idSuffix: "agent-auto-retry") - ) - } - @Test func conditionalAutoNamingAgentDoesNotReuseWorkspaceAutoNamingAnchor() { #expect(SettingsSearchIndex.anchorID(forSettingsPath: "automation.autoNamingAgent") == nil) } diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index 3181c8af6921..d9238875b7da 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -3253,7 +3253,6 @@ final class WorkspaceCreationWorkingDirectoryInheritanceTests: XCTestCase { restorableAgentResumeState: nil, restoredAgentCompletedGeneration: nil, shellActivityState: nil, restoredResumeSessionWorkingDirectory: nil, resumeBinding: resumeBinding, - agentSessionRetryCompletedAttempts: nil, agentRuntime: nil, isRemoteTerminal: false, remoteRelayPort: nil, diff --git a/web/data/cmux.schema.json b/web/data/cmux.schema.json index ac75292b6860..472d25452dbe 100644 --- a/web/data/cmux.schema.json +++ b/web/data/cmux.schema.json @@ -590,12 +590,6 @@ "default": true, "description": "Automatically run agent resume commands for restored terminal sessions when cmux reopens after quit. Set false to restore panes while keeping Claude Code, Codex, OpenCode, and other saved agent sessions idle until you resume them manually." }, - "autoRetryAgentSessions": { - "type": "boolean", - "default": false, - "descriptionKey": "schemaDescriptions.terminal.autoRetryAgentSessions", - "description": "Automatically resume a managed agent session when its active command exits with a nonzero, non-signal error status. Retries are bounded and use exponential backoff; normal completion and explicit user termination are never retried." - }, "showTextBoxOnNewTerminals": { "type": "boolean", "default": false, diff --git a/web/messages/ar.json b/web/messages/ar.json index 175397fece5c..51c24940378e 100644 --- a/web/messages/ar.json +++ b/web/messages/ar.json @@ -957,7 +957,6 @@ "terminal": { "sessionContentMaxWidth": "حد أقصى اختياري للعرض، بالنقاط، لمحتوى الطرفية ودردشة الوكيل المضمنة. عيّن false لاستخدام عرض الجزء بالكامل.", "sessionContentAlignment": "الموضع الأفقي لمحتوى الطرفية ودردشة الوكيل المضمنة عند تفعيل sessionContentMaxWidth.", - "autoRetryAgentSessions": "استأنف تلقائيًا جلسة وكيل مُدارة عندما ينتهي أمرها النشط بحالة خطأ غير صفرية وغير ناتجة عن إشارة. عدد المحاولات محدود ويستخدم تراجعًا أُسّيًا؛ ولا تتم أبدًا إعادة المحاولة عند الاكتمال الطبيعي أو الإنهاء الصريح من المستخدم.", "copyOnSelect": "عند true، ينسخ النص المحدد في الطرفية إلى حافظة النظام عند تثبيت التحديد. عند false، لا يصدر cmux تجاوز Ghostty copy-on-select؛ وتتحكم إعدادات Ghostty وقيمه الافتراضية في سلوك حافظة التحديد.", "scrollSpeed": "مُضاعِف يُطبَّق على مقدار التمرير بعجلة الفأرة ولوحة اللمس في الطرفية. القيم الأعلى تُمرِّر أسرع، والقيم الأدنى تُمرِّر أبطأ.", "showTextBoxOnNewTerminals": "يعرض إدخال TextBox التجريبي افتراضيًا لمساحات العمل وتبويبات الطرفية وتقسيماتها المنشأة حديثًا.", diff --git a/web/messages/bs.json b/web/messages/bs.json index 3777ecf5ef19..fc1adf9b068e 100644 --- a/web/messages/bs.json +++ b/web/messages/bs.json @@ -957,7 +957,6 @@ "terminal": { "sessionContentMaxWidth": "Opcionalna maksimalna širina, u tačkama, za sadržaj terminala i ugrađenog razgovora s agentom. Postavite false za korištenje pune širine okna.", "sessionContentAlignment": "Horizontalni položaj sadržaja terminala i ugrađenog razgovora s agentom kada je sessionContentMaxWidth omogućen.", - "autoRetryAgentSessions": "Automatski nastavlja upravljanu agentsku sesiju kada se njena aktivna naredba završi statusom greške različitim od nule koji nije uzrokovan signalom. Broj ponovnih pokušaja je ograničen i koristi eksponencijalno odgađanje; normalan završetak i izričit prekid korisnika nikada se ne ponavljaju.", "copyOnSelect": "Kada je true, odabrani tekst terminala kopira se u sistemski međuspremnik kada se odabir potvrdi. Kada je false, cmux ne emitira Ghostty copy-on-select nadjačavanje; Ghostty konfiguracija i zadane vrijednosti kontrolišu ponašanje međuspremnika odabira.", "scrollSpeed": "Množitelj koji se primjenjuje na pomak točkića miša i dodirne plohe u terminalu. Veće vrijednosti pomiču brže, a manje sporije.", "showTextBoxOnNewTerminals": "Podrazumijevano prikazuje beta TextBox unos za novokreirane radne prostore, terminalske tabove i podjele.", diff --git a/web/messages/da.json b/web/messages/da.json index 1cd6f4939964..97cfbd142a37 100644 --- a/web/messages/da.json +++ b/web/messages/da.json @@ -957,7 +957,6 @@ "terminal": { "sessionContentMaxWidth": "Valgfri maksimal bredde i punkter for terminalindhold og den indbyggede agentchat. Angiv false for at bruge hele rudens bredde.", "sessionContentAlignment": "Vandret placering af terminalindhold og den indbyggede agentchat, når sessionContentMaxWidth er aktiveret.", - "autoRetryAgentSessions": "Genoptager automatisk en administreret agentsession, når dens aktive kommando afsluttes med en fejlkode, der ikke er nul og ikke skyldes et signal. Antallet af forsøg er begrænset og bruger eksponentiel ventetid; normal afslutning og udtrykkelig brugerafbrydelse forsøges aldrig igen.", "copyOnSelect": "Når værdien er true, kopieres markeret terminaltekst til systemets udklipsholder, når markeringen bekræftes. Når værdien er false, udsender cmux ikke en Ghostty copy-on-select-tilsidesættelse; Ghostty-konfiguration og standarder styrer markeringsudklipsholderens adfærd.", "scrollSpeed": "Multiplikator anvendt på rulleværdier fra musehjul og pegefelt i terminalen. Højere værdier ruller hurtigere, lavere værdier langsommere.", "showTextBoxOnNewTerminals": "Viser beta-TextBox-input som standard for nyoprettede arbejdsområder, terminalfaner og terminalsplits.", diff --git a/web/messages/de.json b/web/messages/de.json index 04c6ffb6048a..e4344755ddb4 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -957,7 +957,6 @@ "terminal": { "sessionContentMaxWidth": "Optionale maximale Breite in Punkten für Terminalinhalte und den integrierten Agentenchat. Mit false wird die gesamte Breite des Bereichs verwendet.", "sessionContentAlignment": "Horizontale Position von Terminalinhalten und integriertem Agentenchat, wenn sessionContentMaxWidth aktiviert ist.", - "autoRetryAgentSessions": "Setzt eine verwaltete Agent-Sitzung automatisch fort, wenn ihr aktiver Befehl mit einem von null verschiedenen Fehlerstatus beendet wird, der nicht durch ein Signal verursacht wurde. Wiederholungsversuche sind begrenzt und verwenden exponentielles Backoff; normale Abschlüsse und ausdrückliche Beendigungen durch den Benutzer werden nie erneut versucht.", "copyOnSelect": "Bei true wird ausgewählter Terminaltext in die Systemzwischenablage kopiert, sobald die Auswahl bestätigt wird. Bei false gibt cmux keine Ghostty-copy-on-select-Überschreibung aus; Ghostty-Konfiguration und Standardwerte steuern das Verhalten der Auswahlzwischenablage.", "scrollSpeed": "Multiplikator für die Scroll-Werte von Mausrad und Trackpad im Terminal. Höhere Werte scrollen schneller, niedrigere Werte langsamer.", "showTextBoxOnNewTerminals": "Zeigt die Beta-TextBox-Eingabe standardmaessig fuer neu erstellte Workspaces, Terminal-Tabs und Terminal-Splits an.", diff --git a/web/messages/en.json b/web/messages/en.json index e85aeee3a962..7d37b43fac0e 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -1814,7 +1814,6 @@ "terminal": { "agentHibernation": "Routine Agent Hibernation settings. cmux kills idle background agent processes to free RAM and CPU, then resumes them with their saved session when their tab is visited. Routine hibernation requires a restorable coding agent whose lifecycle reports idle, an off-screen terminal, a live-terminal count above the configured limit, and unchanged output through the idle and confirmation windows. Independently, during critical memory pressure cmux may hibernate a bounded batch of safe idle background agents even when enabled is false; visible, running, needs-input, recently changed, and unprotectable agents remain excluded. The placeholder Resume button is a manual fallback.", "agentHibernationEnabled": "Enable routine Agent Hibernation based on the live-terminal limit. Critical-pressure safety hibernation remains active when false.", - "autoRetryAgentSessions": "Automatically resume a managed agent session when its active command exits with a nonzero, non-signal error status. Retries are bounded and use exponential backoff; normal completion and explicit user termination are never retried.", "copyOnSelect": "When true, copy selected terminal text to the system clipboard when the selection is committed. When false, cmux does not emit a Ghostty copy-on-select override; Ghostty config and defaults control selection-clipboard behavior.", "scrollSpeed": "Multiplier applied to terminal scroll wheel and trackpad deltas. Higher values scroll faster; lower values scroll slower.", "sessionContentMaxWidth": "Optional maximum width, in points, for terminal and built-in agent chat content. Set false to use the full pane width.", diff --git a/web/messages/es.json b/web/messages/es.json index 27ff07c868fb..dd5cb85c88c5 100644 --- a/web/messages/es.json +++ b/web/messages/es.json @@ -957,7 +957,6 @@ "terminal": { "sessionContentMaxWidth": "Ancho máximo opcional, en puntos, para el contenido del terminal y del chat de agente integrado. Establece false para usar todo el ancho del panel.", "sessionContentAlignment": "Posición horizontal del contenido del terminal y del chat de agente integrado cuando sessionContentMaxWidth está activado.", - "autoRetryAgentSessions": "Reanuda automáticamente una sesión de agente administrada cuando su comando activo termina con un estado de error distinto de cero y no causado por una señal. Los reintentos son limitados y usan retroceso exponencial; nunca se reintentan las finalizaciones normales ni las terminaciones explícitas del usuario.", "copyOnSelect": "Cuando es true, copia el texto seleccionado del terminal al portapapeles del sistema cuando se confirma la selección. Cuando es false, cmux no emite una anulación de copy-on-select de Ghostty; la configuración y los valores predeterminados de Ghostty controlan el comportamiento del portapapeles de selección.", "scrollSpeed": "Multiplicador aplicado a los desplazamientos de la rueda de desplazamiento y del panel táctil en el terminal. Los valores más altos desplazan más rápido y los más bajos, más lento.", "showTextBoxOnNewTerminals": "Muestra la entrada TextBox beta de forma predeterminada en espacios de trabajo, pestañas y divisiones de terminal recién creados.", diff --git a/web/messages/fr.json b/web/messages/fr.json index 011c926ed3cf..14a11151144c 100644 --- a/web/messages/fr.json +++ b/web/messages/fr.json @@ -957,7 +957,6 @@ "terminal": { "sessionContentMaxWidth": "Largeur maximale facultative, en points, pour le contenu du terminal et du chat d’agent intégré. Définissez false pour utiliser toute la largeur du volet.", "sessionContentAlignment": "Position horizontale du contenu du terminal et du chat d’agent intégré lorsque sessionContentMaxWidth est activé.", - "autoRetryAgentSessions": "Reprend automatiquement une session d’agent gérée lorsque sa commande active se termine avec un état d’erreur non nul qui n’est pas causé par un signal. Les tentatives sont limitées et utilisent un délai exponentiel ; les fins normales et les arrêts explicites de l’utilisateur ne sont jamais relancés.", "copyOnSelect": "Lorsque la valeur est true, le texte sélectionné dans le terminal est copié dans le presse-papiers système quand la sélection est validée. Lorsque la valeur est false, cmux n’émet pas de remplacement Ghostty copy-on-select; la configuration et les valeurs par défaut de Ghostty contrôlent le comportement du presse-papiers de sélection.", "scrollSpeed": "Multiplicateur appliqué aux déplacements de la molette de défilement et du pavé tactile dans le terminal. Les valeurs plus élevées font défiler plus vite, les plus basses plus lentement.", "showTextBoxOnNewTerminals": "Affiche l'entrée TextBox bêta par défaut pour les espaces de travail, onglets et divisions de terminal nouvellement créés.", diff --git a/web/messages/it.json b/web/messages/it.json index a529d966e863..273a000e59ac 100644 --- a/web/messages/it.json +++ b/web/messages/it.json @@ -957,7 +957,6 @@ "terminal": { "sessionContentMaxWidth": "Larghezza massima facoltativa, in punti, per il contenuto del terminale e della chat agente integrata. Imposta false per usare l’intera larghezza del riquadro.", "sessionContentAlignment": "Posizionamento orizzontale del contenuto del terminale e della chat agente integrata quando sessionContentMaxWidth è abilitato.", - "autoRetryAgentSessions": "Riprende automaticamente una sessione agente gestita quando il comando attivo termina con uno stato di errore diverso da zero e non causato da un segnale. I tentativi sono limitati e usano un backoff esponenziale; i completamenti normali e le terminazioni esplicite dell’utente non vengono mai ritentati.", "copyOnSelect": "Quando è true, il testo selezionato nel terminale viene copiato negli appunti di sistema quando la selezione viene confermata. Quando è false, cmux non emette un override Ghostty copy-on-select; configurazione e valori predefiniti di Ghostty controllano il comportamento degli appunti di selezione.", "scrollSpeed": "Moltiplicatore applicato agli spostamenti della rotellina di scorrimento e del trackpad nel terminale. Valori più alti scorrono più velocemente, valori più bassi più lentamente.", "showTextBoxOnNewTerminals": "Mostra l'input TextBox beta per impostazione predefinita nei workspace, nelle schede e nelle divisioni del terminale appena creati.", diff --git a/web/messages/ja.json b/web/messages/ja.json index 056c4bf7827f..632d4e6ee471 100644 --- a/web/messages/ja.json +++ b/web/messages/ja.json @@ -1737,7 +1737,6 @@ "terminal": { "agentHibernation": "通常のエージェント休止の設定です。cmux はアイドル中のバックグラウンドエージェントプロセスを終了して RAM と CPU を解放し、そのタブを開いたときに保存済みセッションから再開します。通常の休止には、再開可能なコーディングエージェントであること、ライフサイクルがアイドルと報告していること、ターミナルが画面外にあること、ライブターミナル数が設定上限を超えていること、アイドル時間と確認時間を通して出力が変化していないことが必要です。これとは別に、メモリ負荷が危険な状態では enabled が false でも、安全なアイドル中のバックグラウンドエージェントを cmux が上限付きのバッチで休止する場合があります。表示中、実行中、入力待ち、最近変更された、またはトランスクリプトを保護できないエージェントは対象外です。プレースホルダーの「再開」ボタンは手動の代替手段です。", "agentHibernationEnabled": "ライブターミナル数の上限に基づく通常のエージェント休止を有効にします。false の場合も、メモリ負荷が危険な状態での安全休止は有効です。", - "autoRetryAgentSessions": "アクティブな管理対象エージェントのコマンドが、シグナル以外の 0 でないエラーステータスで終了した場合に、セッションを自動的に再開します。再試行は回数制限付きの指数バックオフで行い、正常終了やユーザーによる明示的な終了は再試行しません。", "copyOnSelect": "true の場合、選択が確定したときにターミナルで選択したテキストをシステムクリップボードへコピーします。false の場合、cmux は Ghostty の copy-on-select 上書きを出力せず、選択クリップボードの動作は Ghostty の設定と既定値に従います。", "scrollSpeed": "ターミナルのスクロールホイールとトラックパッドの移動量に適用される倍率です。値を大きくするとスクロールが速くなり、小さくすると遅くなります。", "sessionContentMaxWidth": "ターミナルと組み込みエージェントチャットのコンテンツ幅の上限をポイント単位で指定します。ペインの全幅を使うには false を設定します。", diff --git a/web/messages/km.json b/web/messages/km.json index 1fa66cee1058..a9d17f59d092 100644 --- a/web/messages/km.json +++ b/web/messages/km.json @@ -957,7 +957,6 @@ "terminal": { "sessionContentMaxWidth": "ទទឹងអតិបរមាជាជម្រើស គិតជាពិន្ទុ សម្រាប់ខ្លឹមសារស្ថានីយ និងការជជែកជាមួយភ្នាក់ងារដែលភ្ជាប់មកជាមួយ។ កំណត់ false ដើម្បីប្រើទទឹងផ្ទាំងទាំងមូល។", "sessionContentAlignment": "ទីតាំងផ្ដេកនៃខ្លឹមសារស្ថានីយ និងការជជែកជាមួយភ្នាក់ងារដែលភ្ជាប់មកជាមួយ នៅពេលបើក sessionContentMaxWidth។", - "autoRetryAgentSessions": "បន្តសម័យភ្នាក់ងារដែលគ្រប់គ្រងដោយស្វ័យប្រវត្តិ នៅពេលពាក្យបញ្ជាសកម្មរបស់វាបញ្ចប់ដោយស្ថានភាពកំហុសមិនសូន្យ និងមិនមែនដោយសញ្ញា។ ការព្យាយាមឡើងវិញមានចំនួនកំណត់ និងប្រើការពន្យារពេលកើនឡើងទ្វេដង; ការបញ្ចប់ធម្មតា និងការបញ្ចប់ដោយចេតនារបស់អ្នកប្រើ មិនត្រូវបានព្យាយាមឡើងវិញឡើយ។", "copyOnSelect": "នៅពេល true អត្ថបទ terminal ដែលបានជ្រើសនឹងត្រូវចម្លងទៅ clipboard របស់ប្រព័ន្ធ ពេលការជ្រើសត្រូវបានបញ្ជាក់។ នៅពេល false cmux មិនបញ្ចេញការកំណត់ជាន់លើ Ghostty copy-on-select ទេ; ការកំណត់ និងលំនាំដើមរបស់ Ghostty គ្រប់គ្រងអាកប្បកិរិយា clipboard សម្រាប់ការជ្រើស។", "scrollSpeed": "មេគុណដែលអនុវត្តចំពោះចលនារមូររបស់កង់រមូរ និងផ្ទាំងប៉ះក្នុង terminal។ តម្លៃកាន់តែខ្ពស់ រមូរកាន់តែលឿន ហើយតម្លៃកាន់តែទាប រមូរកាន់តែយឺត។", "showTextBoxOnNewTerminals": "បង្ហាញការបញ្ចូល TextBox បេតាជាលំនាំដើមសម្រាប់ workspace ផ្ទាំងស្ថានីយ និងស្ប្លីតស្ថានីយដែលទើបបង្កើតថ្មី។", diff --git a/web/messages/ko.json b/web/messages/ko.json index a9f03471044e..2208e8c2a6e1 100644 --- a/web/messages/ko.json +++ b/web/messages/ko.json @@ -957,7 +957,6 @@ "terminal": { "sessionContentMaxWidth": "터미널 및 내장 에이전트 채팅 콘텐츠의 선택적 최대 너비(포인트)입니다. 전체 패널 너비를 사용하려면 false로 설정하세요.", "sessionContentAlignment": "sessionContentMaxWidth가 활성화된 경우 터미널 및 내장 에이전트 채팅 콘텐츠의 가로 배치입니다.", - "autoRetryAgentSessions": "관리되는 에이전트 세션의 활성 명령이 0이 아닌 비신호 오류 상태로 종료되면 세션을 자동으로 재개합니다. 재시도 횟수는 제한되며 지수 백오프를 사용합니다. 정상 완료 및 사용자의 명시적 종료는 절대 재시도하지 않습니다.", "copyOnSelect": "true이면 선택이 확정될 때 선택한 터미널 텍스트를 시스템 클립보드에 복사합니다. false이면 cmux는 Ghostty copy-on-select 재정의를 내보내지 않으며, 선택 클립보드 동작은 Ghostty 설정과 기본값이 제어합니다.", "scrollSpeed": "터미널 스크롤 휠과 트랙패드 이동량에 적용되는 배율입니다. 값이 클수록 빠르게 스크롤되고, 작을수록 느리게 스크롤됩니다.", "showTextBoxOnNewTerminals": "새로 생성된 작업공간, 터미널 탭, 터미널 분할에서 베타 TextBox 입력을 기본으로 표시합니다.", diff --git a/web/messages/no.json b/web/messages/no.json index 7ee1f43c30a5..9b493dce4265 100644 --- a/web/messages/no.json +++ b/web/messages/no.json @@ -957,7 +957,6 @@ "terminal": { "sessionContentMaxWidth": "Valgfri maksimal bredde i punkter for terminalinnhold og den innebygde agentchatten. Angi false for å bruke hele rutens bredde.", "sessionContentAlignment": "Vannrett plassering av terminalinnhold og den innebygde agentchatten når sessionContentMaxWidth er aktivert.", - "autoRetryAgentSessions": "Gjenopptar automatisk en administrert agentøkt når den aktive kommandoen avsluttes med en feilkode som ikke er null og ikke skyldes et signal. Antall forsøk er begrenset og bruker eksponentiell ventetid; normal fullføring og eksplisitt avslutning fra brukeren forsøkes aldri på nytt.", "copyOnSelect": "Når verdien er true, kopieres markert terminaltekst til systemutklippstavlen når markeringen bekreftes. Når verdien er false, sender ikke cmux ut en Ghostty copy-on-select-overstyring; Ghostty-konfigurasjon og standardverdier styrer oppførselen til markeringsutklippstavlen.", "scrollSpeed": "Multiplikator brukt på rulleverdier fra musehjul og styreflate i terminalen. Høyere verdier ruller raskere, lavere verdier saktere.", "showTextBoxOnNewTerminals": "Viser beta-TextBox-inndata som standard for nyopprettede arbeidsområder, terminalfaner og terminalsplitt.", diff --git a/web/messages/pl.json b/web/messages/pl.json index 66b4f16a3928..78efa3026a1b 100644 --- a/web/messages/pl.json +++ b/web/messages/pl.json @@ -958,7 +958,6 @@ "terminal": { "sessionContentMaxWidth": "Opcjonalna maksymalna szerokość w punktach dla zawartości terminala i wbudowanego czatu agenta. Ustaw false, aby użyć pełnej szerokości panelu.", "sessionContentAlignment": "Poziome położenie zawartości terminala i wbudowanego czatu agenta po włączeniu sessionContentMaxWidth.", - "autoRetryAgentSessions": "Automatycznie wznawia zarządzaną sesję agenta, gdy jej aktywne polecenie kończy się niezerowym stanem błędu, który nie wynika z sygnału. Liczba prób jest ograniczona i stosuje wykładnicze opóźnienie; normalne zakończenie i jawne przerwanie przez użytkownika nigdy nie są ponawiane.", "copyOnSelect": "Gdy wartość to true, zaznaczony tekst terminala jest kopiowany do schowka systemowego po zatwierdzeniu zaznaczenia. Gdy wartość to false, cmux nie emituje nadpisania Ghostty copy-on-select; konfiguracja i wartości domyślne Ghostty sterują zachowaniem schowka zaznaczenia.", "scrollSpeed": "Mnożnik stosowany do przewijania kółkiem myszy i gładzikiem w terminalu. Wyższe wartości przewijają szybciej, niższe wolniej.", "showTextBoxOnNewTerminals": "Domyślnie pokazuje wejście TextBox beta dla nowo utworzonych obszarów roboczych, kart terminala i podziałów.", diff --git a/web/messages/pt-BR.json b/web/messages/pt-BR.json index 61fe51b46e27..733e3e776f13 100644 --- a/web/messages/pt-BR.json +++ b/web/messages/pt-BR.json @@ -957,7 +957,6 @@ "terminal": { "sessionContentMaxWidth": "Largura máxima opcional, em pontos, para o conteúdo do terminal e do chat integrado do agente. Defina como false para usar toda a largura do painel.", "sessionContentAlignment": "Posicionamento horizontal do conteúdo do terminal e do chat integrado do agente quando sessionContentMaxWidth está ativado.", - "autoRetryAgentSessions": "Retoma automaticamente uma sessão de agente gerenciada quando o comando ativo termina com um status de erro diferente de zero e não causado por um sinal. As tentativas são limitadas e usam recuo exponencial; conclusões normais e encerramentos explícitos do usuário nunca são repetidos.", "copyOnSelect": "Quando true, copia o texto selecionado no terminal para a área de transferência do sistema quando a seleção é confirmada. Quando false, o cmux não emite uma substituição copy-on-select do Ghostty; a configuração e os padrões do Ghostty controlam o comportamento da área de transferência de seleção.", "scrollSpeed": "Multiplicador aplicado aos deslocamentos da roda de rolagem e do trackpad no terminal. Valores mais altos rolam mais rápido e os mais baixos, mais devagar.", "showTextBoxOnNewTerminals": "Mostra a entrada TextBox beta por padrão em áreas de trabalho, abas e divisões de terminal recém-criadas.", diff --git a/web/messages/ru.json b/web/messages/ru.json index e01d89e668a5..634f96cfeb69 100644 --- a/web/messages/ru.json +++ b/web/messages/ru.json @@ -957,7 +957,6 @@ "terminal": { "sessionContentMaxWidth": "Необязательная максимальная ширина в пунктах для содержимого терминала и встроенного чата агента. Укажите false, чтобы использовать всю ширину панели.", "sessionContentAlignment": "Горизонтальное расположение содержимого терминала и встроенного чата агента при включённом sessionContentMaxWidth.", - "autoRetryAgentSessions": "Автоматически возобновляет управляемый сеанс агента, если его активная команда завершается с ненулевым статусом ошибки, не вызванным сигналом. Число попыток ограничено, а задержка увеличивается экспоненциально; нормальное завершение и явное прерывание пользователем никогда не повторяются.", "copyOnSelect": "Если значение true, выбранный текст терминала копируется в системный буфер обмена после подтверждения выделения. Если значение false, cmux не выводит переопределение Ghostty copy-on-select; конфигурация и значения по умолчанию Ghostty управляют поведением буфера выделения.", "scrollSpeed": "Множитель, применяемый к величине прокрутки колесом мыши и трекпадом в терминале. Чем выше значение, тем быстрее прокрутка, чем ниже — тем медленнее.", "showTextBoxOnNewTerminals": "По умолчанию показывает бета-ввод TextBox для новых рабочих пространств, вкладок терминала и разделений.", diff --git a/web/messages/th.json b/web/messages/th.json index 4cbbb135c170..5367802af08b 100644 --- a/web/messages/th.json +++ b/web/messages/th.json @@ -958,7 +958,6 @@ "terminal": { "sessionContentMaxWidth": "ความกว้างสูงสุดแบบไม่บังคับในหน่วยพอยต์สำหรับเนื้อหาเทอร์มินัลและแชตเอเจนต์ในตัว ตั้งค่าเป็น false เพื่อใช้ความกว้างเต็มพาเนล", "sessionContentAlignment": "ตำแหน่งแนวนอนของเนื้อหาเทอร์มินัลและแชตเอเจนต์ในตัวเมื่อเปิดใช้ sessionContentMaxWidth", - "autoRetryAgentSessions": "กลับมาทำงานต่อในเซสชันเอเจนต์ที่มีการจัดการโดยอัตโนมัติ เมื่อคำสั่งที่ใช้งานอยู่สิ้นสุดด้วยสถานะข้อผิดพลาดที่ไม่ใช่ศูนย์และไม่ได้เกิดจากสัญญาณ จำนวนครั้งที่ลองใหม่มีขีดจำกัดและใช้การหน่วงเวลาแบบเอ็กซ์โพเนนเชียล การเสร็จสิ้นตามปกติและการยุติโดยผู้ใช้อย่างชัดเจนจะไม่ถูกลองใหม่", "copyOnSelect": "เมื่อเป็น true ข้อความเทอร์มินัลที่เลือกจะถูกคัดลอกไปยังคลิปบอร์ดของระบบเมื่อยืนยันการเลือก เมื่อเป็น false cmux จะไม่ส่งค่าแทนที่ Ghostty copy-on-select; การกำหนดค่าและค่าเริ่มต้นของ Ghostty จะควบคุมพฤติกรรมของคลิปบอร์ดการเลือก", "scrollSpeed": "ตัวคูณที่ใช้กับระยะการเลื่อนของล้อเลื่อนเมาส์และแทร็กแพดในเทอร์มินัล ค่ายิ่งสูงยิ่งเลื่อนเร็ว ค่ายิ่งต่ำยิ่งเลื่อนช้า", "showTextBoxOnNewTerminals": "แสดงอินพุต TextBox รุ่นเบต้าเป็นค่าเริ่มต้นสำหรับเวิร์กสเปซ แท็บเทอร์มินัล และการแบ่งเทอร์มินัลที่สร้างใหม่", diff --git a/web/messages/tr.json b/web/messages/tr.json index 1d2393b65742..0a5ea8a7b374 100644 --- a/web/messages/tr.json +++ b/web/messages/tr.json @@ -957,7 +957,6 @@ "terminal": { "sessionContentMaxWidth": "Terminal ve yerleşik ajan sohbeti içeriği için puan cinsinden isteğe bağlı maksimum genişlik. Bölmenin tamamını kullanmak için false olarak ayarlayın.", "sessionContentAlignment": "sessionContentMaxWidth etkin olduğunda terminal ve yerleşik ajan sohbeti içeriğinin yatay konumu.", - "autoRetryAgentSessions": "Yönetilen bir ajan oturumunun etkin komutu sıfır olmayan ve sinyal kaynaklı olmayan bir hata durumuyla sona erdiğinde oturumu otomatik olarak sürdürür. Yeniden denemeler sınırlıdır ve üstel geri çekilme kullanır; normal tamamlanmalar ve kullanıcının açıkça sonlandırdığı oturumlar hiçbir zaman yeniden denenmez.", "copyOnSelect": "true olduğunda, seçim kesinleştiğinde seçilen terminal metni sistem panosuna kopyalanır. false olduğunda cmux bir Ghostty copy-on-select geçersiz kılması yaymaz; seçim panosu davranışını Ghostty yapılandırması ve varsayılanları kontrol eder.", "scrollSpeed": "Terminalde fare tekerleği ve dokunmatik yüzey kaydırma değerlerine uygulanan çarpan. Yüksek değerler daha hızlı, düşük değerler daha yavaş kaydırır.", "showTextBoxOnNewTerminals": "Yeni oluşturulan çalışma alanları, terminal sekmeleri ve terminal bölmeleri için beta TextBox girişini varsayılan olarak gösterir.", diff --git a/web/messages/uk.json b/web/messages/uk.json index b2bd2a0cbaec..03aa0b1f857d 100644 --- a/web/messages/uk.json +++ b/web/messages/uk.json @@ -957,7 +957,6 @@ "terminal": { "sessionContentMaxWidth": "Необов’язкова максимальна ширина в пунктах для вмісту термінала та вбудованого чату агента. Значення false використовує всю ширину панелі.", "sessionContentAlignment": "Горизонтальне розташування вмісту термінала та вбудованого чату агента, коли ввімкнено sessionContentMaxWidth.", - "autoRetryAgentSessions": "Автоматично відновлює керований сеанс агента, якщо його активна команда завершується з ненульовим станом помилки, не спричиненим сигналом. Кількість спроб обмежена, а затримка зростає експоненційно; нормальне завершення та явне припинення користувачем ніколи не повторюються.", "copyOnSelect": "Якщо значення true, вибраний текст термінала копіюється до системного буфера обміну після підтвердження виділення. Якщо значення false, cmux не виводить перевизначення Ghostty copy-on-select; конфігурація та стандартні значення Ghostty керують поведінкою буфера виділення.", "scrollSpeed": "Множник, що застосовується до величини прокручування коліщатком миші та трекпадом у терміналі. Вищі значення прокручують швидше, нижчі — повільніше.", "showTextBoxOnNewTerminals": "Типово показує бета-ввід TextBox для новостворених робочих просторів, вкладок термінала й розділень.", diff --git a/web/messages/zh-CN.json b/web/messages/zh-CN.json index c5fd73053567..0787106d9fca 100644 --- a/web/messages/zh-CN.json +++ b/web/messages/zh-CN.json @@ -957,7 +957,6 @@ "terminal": { "sessionContentMaxWidth": "终端和内置代理聊天内容的可选最大宽度,单位为点。设为 false 可使用整个窗格宽度。", "sessionContentAlignment": "启用 sessionContentMaxWidth 时,终端和内置代理聊天内容的水平位置。", - "autoRetryAgentSessions": "当托管代理会话的活动命令以非零且非信号错误状态退出时,自动恢复该会话。重试次数有限,并采用指数退避;正常完成和用户明确终止绝不会重试。", "copyOnSelect": "为 true 时,在确认选择后将所选终端文本复制到系统剪贴板。为 false 时,cmux 不会输出 Ghostty copy-on-select 覆盖项;选择剪贴板行为由 Ghostty 配置和默认值控制。", "scrollSpeed": "应用于终端滚轮和触控板滚动量的倍数。值越大滚动越快,值越小滚动越慢。", "showTextBoxOnNewTerminals": "默认在新建的工作区、终端标签页和终端分屏中显示测试版 TextBox 输入。", diff --git a/web/messages/zh-TW.json b/web/messages/zh-TW.json index e481fcab52e7..ce851f12c333 100644 --- a/web/messages/zh-TW.json +++ b/web/messages/zh-TW.json @@ -957,7 +957,6 @@ "terminal": { "sessionContentMaxWidth": "終端機與內建代理程式聊天內容的選用最大寬度,單位為點。設為 false 可使用整個窗格寬度。", "sessionContentAlignment": "啟用 sessionContentMaxWidth 時,終端機與內建代理程式聊天內容的水平位置。", - "autoRetryAgentSessions": "當受管理的代理程式工作階段中的作用中命令以非零且非訊號的錯誤狀態結束時,自動繼續該工作階段。重試次數有限並採用指數退避;正常完成和使用者明確終止絕不會重試。", "copyOnSelect": "為 true 時,選取確認後會將所選終端機文字複製到系統剪貼簿。為 false 時,cmux 不會輸出 Ghostty copy-on-select 覆寫;選取剪貼簿行為由 Ghostty 設定與預設值控制。", "scrollSpeed": "套用於終端機滾輪與觸控板捲動量的倍率。數值越大捲動越快,數值越小捲動越慢。", "showTextBoxOnNewTerminals": "預設在新建的工作區、終端機分頁和終端機分割中顯示測試版 TextBox 輸入。",