Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 69 additions & 28 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30678,11 +30678,17 @@ struct CMUXCLI {
}
}

/// Watches the Codex rollout until the turn settles.
///
/// Returns the Stop replay for a healthy completion instead of running it,
/// so the caller runs the Stop event after this frame has unwound. See
/// `runGenericAgentHook`. Kept out of line so its locals are gone before
/// the replayed Stop runs.
@inline(never)
private func runCodexTranscriptMonitor(
commandArgs: [String],
client: SocketClient,
replayStop: (CodexTranscriptMonitorStopReplay) throws -> Void
) throws {
client: SocketClient
) -> CodexTranscriptMonitorStopReplay? {
let env = ProcessInfo.processInfo.environment
let workspaceId = optionValue(commandArgs, name: "--workspace") ?? env["CMUX_WORKSPACE_ID"] ?? ""
let surfaceId = optionValue(commandArgs, name: "--surface") ?? env["CMUX_SURFACE_ID"]
Expand All @@ -30697,7 +30703,7 @@ struct CMUXCLI {

guard !workspaceId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
!sessionId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return
return nil
}

defer { removeCodexMonitorLease(path: leasePath) }
Expand All @@ -30706,13 +30712,13 @@ struct CMUXCLI {
var publishedUserInputCallIds = Set<String>()
while Date() < deadline {
if isCodexMonitorLeaseRetired(path: leasePath) {
return
return nil
}
let now = Date()
if now >= nextOwnerCheck {
nextOwnerCheck = now.addingTimeInterval(Self.codexMonitorOwnerCheckIntervalSeconds)
if codexMonitorOwnerState(workspaceId: workspaceId, surfaceId: surfaceId, client: client) == .gone {
return
return nil
}
}

Expand Down Expand Up @@ -30741,19 +30747,16 @@ struct CMUXCLI {
surfaceId: surfaceId,
client: client
)
return
return nil
case .healthy(let lastAssistantMessage):
if let replay = CodexTranscriptMonitorStopReplay(
return CodexTranscriptMonitorStopReplay(
sessionId: sessionId,
turnId: turnId,
transcriptPath: currentTranscriptPath,
workspaceId: workspaceId,
surfaceId: surfaceId,
lastAssistantMessage: lastAssistantMessage
) {
try replayStop(replay)
}
return
)
case .pending:
break
case .unavailable:
Expand All @@ -30769,9 +30772,10 @@ struct CMUXCLI {
}

let remaining = deadline.timeIntervalSinceNow
guard remaining > 0 else { return }
guard remaining > 0 else { return nil }
waitForCodexTranscriptChange(path: transcriptPath, leasePath: leasePath, timeout: min(30, remaining))
}
return nil
}

private func publishCodexMonitorUserInput(
Expand Down Expand Up @@ -33827,6 +33831,16 @@ export default CMUXSessionRestore;
return normalizedHookValue(env["CMUX_SURFACE_ID"]) ?? ""
}

/// Runs one agent hook event.
///
/// `codex monitor` is the one event that leads to another: when the
/// rollout completes, the monitor replays a Stop event. That replay must
/// not run inside the event handler's own frame. The handler frame is very
/// large (about 175 KB of inlined locals), and the CLI runs on a Swift
/// concurrency thread with a 512 KB stack, so two nested handler frames
/// overflowed it (SIGBUS). The monitor returns the replay, and this
/// dispatcher runs it after the monitor returns, so at most one handler
/// frame is live at a time.
private func runGenericAgentHook(
def: AgentHookDef,
commandArgs: [String],
Expand All @@ -33835,6 +33849,48 @@ export default CMUXSessionRestore;
socketPassword: String? = nil,
rawInputOverride: String? = nil,
hookDeadline: Date? = nil
) throws {
guard def.name == "codex", commandArgs.first?.lowercased() == "monitor" else {
try runGenericAgentHookEvent(
def: def,
commandArgs: commandArgs,
client: client,
telemetry: telemetry,
socketPassword: socketPassword,
rawInputOverride: rawInputOverride,
hookDeadline: hookDeadline
)
return
}
telemetry.breadcrumb("\(def.name)-hook.monitor")
guard let replay = runCodexTranscriptMonitor(
commandArgs: Array(commandArgs.dropFirst()),
client: client
) else {
return
}
try runGenericAgentHookEvent(
def: def,
commandArgs: replay.commandArguments,
client: client,
telemetry: telemetry,
socketPassword: socketPassword,
rawInputOverride: replay.payload,
hookDeadline: hookDeadline
)
}

/// Handles a single agent hook event. Kept out of line so its large frame
/// is never merged into `runGenericAgentHook`, and never re-entered.
@inline(never)
private func runGenericAgentHookEvent(
def: AgentHookDef,
commandArgs: [String],
client: SocketClient,
telemetry: CLISocketSentryTelemetry,
socketPassword: String?,
rawInputOverride: String?,
hookDeadline: Date?
) throws {
let env = ProcessInfo.processInfo.environment
let skipCodexLegacyPromptStop = env["CMUX_CODEX_SETTLED_CHILD_STOP"] == "1"
Expand Down Expand Up @@ -33874,21 +33930,6 @@ export default CMUXSessionRestore;
}
}

if def.name == "codex", subcommand == "monitor" {
try runCodexTranscriptMonitor(commandArgs: hookArgs, client: client) { replay in
try runGenericAgentHook(
def: def,
commandArgs: replay.commandArguments,
client: client,
telemetry: telemetry,
socketPassword: socketPassword,
rawInputOverride: replay.payload,
hookDeadline: hookDeadline
)
}
return
}

if def.name == "codex", subcommand == "sync-native-title" {
runCodexNativeTitleSyncHook(
commandArgs: hookArgs,
Expand Down
Loading