Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
29f3451
fix: separate Codex launch and worktree paths
mplibunao Jul 15, 2026
9d09d32
fix: harden Codex app-server process lifecycle
mplibunao Jul 15, 2026
8dfb63f
test: cover Codex worktree path separation
mplibunao Jul 15, 2026
97f9339
fix: scope worktree selectors to declared repo
mplibunao Jul 16, 2026
b7e0e7f
fix: preserve Codex exit evidence under load
mplibunao Jul 16, 2026
0303d79
fix: harden Codex startup lifecycle boundaries
mplibunao Jul 16, 2026
4b47ce1
fix: bound Codex reaper wait-failure retries
mplibunao Jul 16, 2026
a6adc5e
refactor: consolidate Codex controller teardown core
mplibunao Jul 16, 2026
6622cfc
refactor: unify Codex termination-task arbitration
mplibunao Jul 16, 2026
b53e70b
refactor: keep Codex path pair whole and clarify observer naming
mplibunao Jul 16, 2026
00f8c25
refactor: hand claim context to termination task immutably
mplibunao Jul 16, 2026
8d86377
fix: self-heal missed child exit notifications
mplibunao Jul 16, 2026
f65e925
test: align Codex readiness coverage with path pairs
mplibunao Jul 17, 2026
3bfaec2
fix: stabilize SwiftFormat policy across versions
mplibunao Jul 17, 2026
1654a26
fix: preserve replacement controller during stale teardown
mplibunao Jul 17, 2026
96872f7
Merge upstream main into Codex worktree startup fix
mplibunao Jul 18, 2026
d42722c
Merge remote-tracking branch 'upstream/main' into bugfix/codex-worktr…
mplibunao Jul 22, 2026
2f995b6
test: widen process termination fixture deadline
mplibunao Jul 22, 2026
33002fb
Merge upstream/main into bugfix/codex-worktree-app-server-start
mplibunao Jul 22, 2026
759ddfe
fix(codex): preserve resume rollout path
mplibunao Jul 22, 2026
106ec69
chore: retrigger CI
mplibunao Jul 22, 2026
07c6899
Merge current main into PR #568
baron Jul 22, 2026
7cc60fd
fix: fence Codex startup after shutdown
baron Jul 22, 2026
f53a5ef
fix: preserve primary root for git-style selectors
baron Jul 22, 2026
21786ec
test: stabilize nested primary selector coverage
baron Jul 22, 2026
27fa42c
Merge current main into PR #568
baron Jul 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions Scripts/Fixtures/test-suite-contract-ledger.tsv

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Foundation

/// Launch/execution directory pair for one Codex controller.
///
/// A worktree-bound session launches the Codex app-server from the workspace's logical root so
/// process startup behaves exactly like an unbound session, while thread and turn execution still
/// target the bound worktree. Unbound sessions use the same directory for both roles.
struct CodexRuntimeWorkspacePaths: Equatable {
/// Directory the Codex app-server process launches from.
let processLaunchDirectory: String?
/// Directory Codex thread/turn execution and the primary sandbox writable root target.
let executionDirectory: String?

private init(processLaunchDirectory: String?, executionDirectory: String?) {
self.processLaunchDirectory = processLaunchDirectory
self.executionDirectory = executionDirectory
}

/// Both roles share one directory — the shape of every session without a primary
/// worktree binding.
static func uniform(_ path: String?) -> CodexRuntimeWorkspacePaths {
CodexRuntimeWorkspacePaths(processLaunchDirectory: path, executionDirectory: path)
}

/// A validated worktree binding keeps process launch anchored to its logical workspace while
/// routing thread and turn execution through the worktree.
static func worktreeBound(
logicalRootPath: String,
validatedWorktreeRootPath: String
) -> CodexRuntimeWorkspacePaths {
CodexRuntimeWorkspacePaths(
processLaunchDirectory: logicalRootPath,
executionDirectory: validatedWorktreeRootPath
)
}
}

/// Validation failures for the Codex launch-directory projection.
///
/// Thrown before provider startup so a broken selected logical root is as actionable as an
/// unavailable worktree (`AgentWorktreeRuntimeWorkspaceError`).
enum CodexRuntimeWorkspacePathsError: LocalizedError, Equatable {
case emptyLogicalRoot
case launchDirectoryUnavailable(path: String)

var errorDescription: String? {
switch self {
case .emptyLogicalRoot:
"Agent session is bound to a worktree whose workspace root path is empty. Rebind the session to a worktree of a loaded workspace root, or unbind the session before starting the agent."
case let .launchDirectoryUnavailable(path):
"Agent session is bound to a worktree for workspace root '\(path)', but that root directory is unavailable. Restore the workspace root, bind the session to another worktree, or unbind the session before starting the agent."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,32 +26,61 @@ enum AgentWorktreeRuntimeWorkspaceResolver {
guard let binding else {
return primaryWorkspacePath
}
let worktreePath = standardizedWorkspacePath(binding.worktreeRootPath)
guard let worktreePath else {
throw AgentWorktreeRuntimeWorkspaceError(binding: binding)
return try validatedWorktreeRootPath(for: binding)
}

/// Codex-specific projection of the same primary-binding selection used by
/// `effectiveWorkspacePath`: the app-server process launches from the binding's logical root
/// while thread/turn execution targets the bound worktree. The logical root is validated
/// eagerly so a launch-directory failure surfaces before provider startup instead of as an
/// opaque process-spawn error.
static func codexRuntimeWorkspacePaths(
bindings: [AgentSessionWorktreeBinding],
fallbackWorkspacePath: String?
) throws -> CodexRuntimeWorkspacePaths {
let primaryWorkspacePath = standardizedWorkspacePath(fallbackWorkspacePath)
let binding = primaryExecutionBinding(
in: bindings,
fallbackWorkspacePath: fallbackWorkspacePath
)

guard let binding else {
return .uniform(primaryWorkspacePath)
}
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: worktreePath, isDirectory: &isDirectory),
isDirectory.boolValue
else {
throw AgentWorktreeRuntimeWorkspaceError(binding: binding)
let executionDirectory = try validatedWorktreeRootPath(for: binding)
guard let processLaunchDirectory = standardizedWorkspacePath(binding.logicalRootPath) else {
throw CodexRuntimeWorkspacePathsError.emptyLogicalRoot
}
return worktreePath
guard directoryExists(atPath: processLaunchDirectory) else {
throw CodexRuntimeWorkspacePathsError.launchDirectoryUnavailable(path: processLaunchDirectory)
}
return .worktreeBound(
logicalRootPath: processLaunchDirectory,
validatedWorktreeRootPath: executionDirectory
)
}

static func validateBindingsAvailable(_ bindings: [AgentSessionWorktreeBinding]) throws {
for binding in bindings {
let worktreePath = standardizedWorkspacePath(binding.worktreeRootPath)
guard let worktreePath else {
throw AgentWorktreeRuntimeWorkspaceError(binding: binding)
}
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: worktreePath, isDirectory: &isDirectory),
isDirectory.boolValue
else {
throw AgentWorktreeRuntimeWorkspaceError(binding: binding)
}
_ = try validatedWorktreeRootPath(for: binding)
}
}

private static func validatedWorktreeRootPath(
for binding: AgentSessionWorktreeBinding
) throws -> String {
guard let worktreePath = standardizedWorkspacePath(binding.worktreeRootPath),
directoryExists(atPath: worktreePath)
else {
throw AgentWorktreeRuntimeWorkspaceError(binding: binding)
}
return worktreePath
}

private static func directoryExists(atPath path: String) -> Bool {
var isDirectory: ObjCBool = false
return FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory)
&& isDirectory.boolValue
}

static func standardizedWorkspacePath(_ path: String?) -> String? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,9 +496,10 @@ extension AgentModeViewModel {
/// The task label kind the current Codex controller was created with.
/// Used to detect when role-specific native tool overrides require controller recycling.
var codexControllerTaskLabelKind: AgentModelCatalog.TaskLabelKind?
/// The effective workspace path the current Codex controller was created with.
/// Used to recycle the provider when a session worktree binding changes cwd.
var codexControllerWorkspacePath: String?
/// The launch/execution directory pair the current Codex controller was created with.
/// Controller replacement key: the provider is recycled when either directory changes,
/// e.g. when a session worktree binding moves the execution cwd.
var codexControllerWorkspacePaths: CodexRuntimeWorkspacePaths?
struct CodexControllerFeatureState: Equatable {
var computerUseEnabled: Bool
var goalSupportEnabled: Bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ final class AgentModeViewModel: ObservableObject {
_ runID: UUID,
_ tabID: UUID,
_ windowID: Int,
_ workspacePath: String?,
_ workspacePaths: CodexRuntimeWorkspacePaths,
_ permissionProfile: AgentPermissionProfile,
_ taskLabelKind: AgentModelCatalog.TaskLabelKind?
) -> any CodexSessionControlling
Expand Down Expand Up @@ -1413,20 +1413,16 @@ final class AgentModeViewModel: ObservableObject {
let codexWorkspacePathProvider = { [weak workspaceManager] in
workspaceManager?.activeWorkspace?.repoPaths.first
}
let sessionWorkspacePathProvider: (TabSession) throws -> String? = { session in
try Self.effectiveWorkspacePath(
for: session,
fallbackWorkspacePath: codexWorkspacePathProvider()
)
}
let (sessionWorkspacePathProvider, codexRuntimeWorkspacePathsProvider) =
Self.makeSessionWorkspaceProviders(fallbackWorkspacePath: codexWorkspacePathProvider)
workspacePathProvider = codexWorkspacePathProvider
attachmentWorkspaceDirectoryProvider = { [weak workspaceManager] in
guard let workspaceManager, workspaceManager.activeWorkspace != nil else {
return nil
}
return FileManager.default.temporaryDirectory
}
let codexControllerFactory: CodexAgentModeCoordinator.CodexControllerFactory = { runID, tabID, windowID, workspacePath, permissionProfile, _, computerUseEnabled in
let codexControllerFactory: CodexAgentModeCoordinator.CodexControllerFactory = { runID, tabID, windowID, workspacePaths, permissionProfile, _, computerUseEnabled in
let client = CodexAppServerClient()
let options = CodexNativeSessionController.Options.agentModeDefault(
approvalPolicyProvider: { permissionProfile.codexApprovalPolicy },
Expand All @@ -1443,7 +1439,7 @@ final class AgentModeViewModel: ObservableObject {
runID: runID,
tabID: tabID,
windowID: windowID,
workspacePath: workspacePath,
workspacePaths: workspacePaths,
options: options,
clientShutdownBehavior: .stopOnShutdown,
expectedMCPClientName: AgentProviderKind.codexExec.mcpClientNameHint
Expand Down Expand Up @@ -1491,7 +1487,7 @@ final class AgentModeViewModel: ObservableObject {
usesProductionAgentDefaultsAndModelPolling = true
codexCoordinator = CodexAgentModeCoordinator(
windowID: windowID,
workspacePathProvider: sessionWorkspacePathProvider,
runtimeWorkspacePathsProvider: codexRuntimeWorkspacePathsProvider,
codexControllerFactory: codexControllerFactory,
connectionPolicyInstaller: connectionPolicyInstaller,
shouldManageCodexTooling: true,
Expand Down Expand Up @@ -1640,16 +1636,12 @@ final class AgentModeViewModel: ObservableObject {
return FileManager.default.temporaryDirectory
}
let codexWorkspacePathProvider = { testWorkspacePath }
let sessionWorkspacePathProvider: (TabSession) throws -> String? = { session in
try Self.effectiveWorkspacePath(
for: session,
fallbackWorkspacePath: codexWorkspacePathProvider()
)
}
let (sessionWorkspacePathProvider, codexRuntimeWorkspacePathsProvider) =
Self.makeSessionWorkspaceProviders(fallbackWorkspacePath: codexWorkspacePathProvider)
workspacePathProvider = codexWorkspacePathProvider
let codexControllerFactory: CodexAgentModeCoordinator.CodexControllerFactory = codexControllerFactoryWithComputerUse
?? { runID, tabID, windowID, workspacePath, permissionProfile, taskLabelKind, _ in
codexControllerFactory(runID, tabID, windowID, workspacePath, permissionProfile, taskLabelKind)
?? { runID, tabID, windowID, workspacePaths, permissionProfile, taskLabelKind, _ in
codexControllerFactory(runID, tabID, windowID, workspacePaths, permissionProfile, taskLabelKind)
}
self.headlessProviderFactory = headlessProviderFactory
self.acpProviderFactory = acpProviderFactory
Expand All @@ -1674,7 +1666,7 @@ final class AgentModeViewModel: ObservableObject {
}
codexCoordinator = CodexAgentModeCoordinator(
windowID: testWindowID,
workspacePathProvider: sessionWorkspacePathProvider,
runtimeWorkspacePathsProvider: codexRuntimeWorkspacePathsProvider,
codexControllerFactory: codexControllerFactory,
connectionPolicyInstaller: connectionPolicyInstaller,
shouldManageCodexTooling: shouldManageCodexTooling,
Expand Down Expand Up @@ -1843,6 +1835,13 @@ final class AgentModeViewModel: ObservableObject {
)
}

func codexRuntimeWorkspacePaths(for session: TabSession) throws -> CodexRuntimeWorkspacePaths {
try Self.codexRuntimeWorkspacePaths(
for: session,
fallbackWorkspacePath: workspacePathProvider()
)
}

func primaryExecutionBinding(for session: TabSession) -> AgentSessionWorktreeBinding? {
Self.primaryExecutionBinding(in: session.worktreeBindings, fallbackWorkspacePath: workspacePathProvider())
}
Expand Down Expand Up @@ -1878,6 +1877,41 @@ final class AgentModeViewModel: ObservableObject {
)
}

/// One projection contract for both initializers: session-scoped scalar
/// workspace path and Codex launch/execution path pair, derived from the
/// same fallback workspace provider.
private static func makeSessionWorkspaceProviders(
fallbackWorkspacePath: @escaping () -> String?
) -> (
sessionWorkspacePath: (TabSession) throws -> String?,
codexRuntimeWorkspacePaths: (TabSession) throws -> CodexRuntimeWorkspacePaths
) {
(
sessionWorkspacePath: { session in
try Self.effectiveWorkspacePath(
for: session,
fallbackWorkspacePath: fallbackWorkspacePath()
)
},
codexRuntimeWorkspacePaths: { session in
try Self.codexRuntimeWorkspacePaths(
for: session,
fallbackWorkspacePath: fallbackWorkspacePath()
)
}
)
}

private static func codexRuntimeWorkspacePaths(
for session: TabSession,
fallbackWorkspacePath: String?
) throws -> CodexRuntimeWorkspacePaths {
try AgentWorktreeRuntimeWorkspaceResolver.codexRuntimeWorkspacePaths(
bindings: session.worktreeBindings,
fallbackWorkspacePath: fallbackWorkspacePath
)
}

private static func standardizedWorkspacePath(_ path: String?) -> String? {
AgentWorktreeRuntimeWorkspaceResolver.standardizedWorkspacePath(path)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1425,7 +1425,7 @@
runID: UUID(),
tabID: UUID(),
windowID: windowID,
workspacePath: configuration.workspaceRootPaths.first
workspacePaths: .uniform(configuration.workspaceRootPaths.first)
)
let recorder = PersistedCodexFixtureEventRecorder()
let eventTask = Task {
Expand Down
Loading
Loading