Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
aaa7c4e
test: reject unindexed Codex review restore IDs
lawrencecchen Jul 17, 2026
97203e4
fix: require Codex-owned restore sessions
lawrencecchen Jul 17, 2026
df802cb
Merge remote-tracking branch 'origin/main' into feat-durable-codex-se…
lawrencecchen Jul 17, 2026
01fbee4
Merge remote-tracking branch 'origin/main' into feat-durable-codex-se…
lawrencecchen Jul 24, 2026
6e949f8
test: reproduce indexed Codex subagent restore takeover
lawrencecchen Jul 25, 2026
bb1022b
fix: resolve Codex subagents to restore roots
lawrencecchen Jul 25, 2026
2d7469e
chore: keep restore fix scoped
lawrencecchen Jul 25, 2026
71372d0
fix: verify detached split restore inputs
lawrencecchen Jul 25, 2026
e740f7f
Merge remote-tracking branch 'origin/main' into feat-durable-codex-se…
lawrencecchen Jul 25, 2026
50a4e21
test: reject indexed codex exec workers
lawrencecchen Jul 26, 2026
e0b687f
fix: keep codex exec workers off restore bindings
lawrencecchen Jul 26, 2026
bcec85d
test: expose stale codex thread cache
lawrencecchen Jul 26, 2026
72885ea
fix: refresh codex restore ownership per lookup
lawrencecchen Jul 26, 2026
2c0463c
test: preserve live codex restore bindings
lawrencecchen Jul 26, 2026
d3af20b
fix: keep live codex restore bindings
lawrencecchen Jul 26, 2026
346b6e0
test: preserve codex executable after resume
lawrencecchen Jul 26, 2026
5317803
fix: retain codex executable after resume
lawrencecchen Jul 26, 2026
67caf90
test: preserve codex binding across empty scans
lawrencecchen Jul 26, 2026
e78b706
fix: keep agent restore identity across scans
lawrencecchen Jul 26, 2026
62cff23
test: rebind resumed codex to current pid
lawrencecchen Jul 26, 2026
cfe8d7e
fix: rebind resumed agents to current pid
lawrencecchen Jul 26, 2026
209e39c
test: close Codex restore evidence gaps
lawrencecchen Jul 26, 2026
e8ee02a
fix: verify legacy Codex restore bindings
lawrencecchen Jul 26, 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
45 changes: 45 additions & 0 deletions CLI/CMUXCLI+AgentHookRestoreEvidence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,43 @@ import CMUXAgentLaunch
extension CMUXCLI {
private static let codexPermissionEvidenceChunkBytes = 64 * 1024

func agentHookProviderOwnsResumeTarget(
kind: String,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Stale thread-index cache for long-lived process

codexSessionResumeVerifier is a process-lifetime static with an internal CodexThreadIndexCache that loads state_5.sqlite once per database path and never re-reads it. Any Codex session created after the first hook fires won't appear in the frozen dictionary; those hooks fall through to the transcriptPath / legacyRollout path. For modern Codex hooks that include transcript_path this is a silent degradation, but for hooks that don't include it (e.g. stripped environments) the valid new session is incorrectly rejected as a review UUID. A targeted re-query on cache miss — rather than treating an empty result as final — would keep the cache warm for subsequent look-ups while still loading new entries that Codex indexed after startup.

sessionId: String,
transcriptPath: String?,
launchCommand: AgentHookLaunchCommandRecord?
) -> Bool {
guard let sessionId = normalizedHookValue(sessionId) else { return false }
return agentHookCanonicalResumeSessionId(
kind: kind,
sessionId: sessionId,
transcriptPath: transcriptPath,
launchCommand: launchCommand
) == sessionId
}

func agentHookCanonicalResumeSessionId(
kind: String,
sessionId: String,
transcriptPath: String?,
launchCommand: AgentHookLaunchCommandRecord?
) -> String? {
guard let sessionId = normalizedHookValue(sessionId) else { return nil }
guard kind == "codex" else { return sessionId }
let environment = ProcessInfo.processInfo.environment
let codexHome = normalizedHookValue(launchCommand?.environment?["CODEX_HOME"])
?? normalizedHookValue(environment["CODEX_HOME"])
?? URL(
fileURLWithPath: normalizedHookValue(environment["HOME"]) ?? NSHomeDirectory(),
isDirectory: true
).appendingPathComponent(".codex", isDirectory: true).path
return CodexSessionResumeVerifier().evidence(
sessionId: sessionId,
transcriptPath: transcriptPath,
codexHome: codexHome
)?.sessionId
}

private func codexLaunchHasExplicitPermissions(_ launchCommand: AgentHookLaunchCommandRecord?) -> Bool {
guard let launchCommand,
AgentLaunchCaptureTrust.launcherDescribesKind(launchCommand.launcher, kind: "codex") else {
Expand Down Expand Up @@ -223,6 +260,14 @@ extension CMUXCLI {
guard !permissionArguments.isEmpty else {
return launchCommand
}
// Environment-only captures intentionally have no argv. Establish the
// logical executable before appending repaired flags, or the first flag
// becomes argv[0] and the saved command tries to execute `--yolo`.
if launchCommand.arguments.isEmpty {
launchCommand.arguments = [
normalizedHookValue(launchCommand.executablePath) ?? "codex"
]
}
launchCommand.arguments.append(contentsOf: permissionArguments)
return launchCommand
}
Expand Down
5 changes: 5 additions & 0 deletions CLI/CMUXCLI+SessionsList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,11 @@ extension CMUXCLI {
let launchBacked = record.launchCommand != nil && agentHookSessionHasDurableResumeEvidence(
kind: spec.name,
launchCommand: record.launchCommand
) && agentHookProviderOwnsResumeTarget(
kind: spec.name,
sessionId: record.sessionId,
transcriptPath: record.transcriptPath,
launchCommand: record.launchCommand
)
payload["launch_backed"] = launchBacked

Expand Down
55 changes: 40 additions & 15 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27689,9 +27689,19 @@ struct CMUXCLI {
sessionId: String,
cwd: String?,
launchCommand: AgentHookLaunchCommandRecord?,
transcriptPath: String? = nil,
observedPermissionMode: String? = nil
) {
if !agentHookSessionHasDurableResumeEvidence(kind: kind, launchCommand: launchCommand) {
guard agentHookSessionHasDurableResumeEvidence(
kind: kind,
launchCommand: launchCommand
),
let resumeSessionId = agentHookCanonicalResumeSessionId(
kind: kind,
sessionId: sessionId,
transcriptPath: transcriptPath,
launchCommand: launchCommand
) else {
clearAgentSurfaceResumeBinding(client: client, workspaceId: workspaceId, surfaceId: surfaceId, sessionId: sessionId)
return
}
Expand All @@ -27704,7 +27714,7 @@ struct CMUXCLI {
)
guard let command = agentSurfaceResumeCommand(
kind: kind,
sessionId: sessionId,
sessionId: resumeSessionId,
launchCommand: launchCommand,
workingDirectory: resumeWorkingDirectory,
environment: resumeEnvironment,
Expand All @@ -27723,7 +27733,7 @@ struct CMUXCLI {
"surface_id": surfaceId,
"name": displayName,
"kind": kind,
"checkpoint_id": sessionId,
"checkpoint_id": resumeSessionId,
"source": "agent-hook",
"command": command,
"auto_resume": true
Expand Down Expand Up @@ -30150,7 +30160,14 @@ export default CMUXSessionRestore;
// Workspace/surface resolution: prefer --workspace/--surface flags,
// then env, then the caller process. Grok strips CMUX_* from hook
// subprocesses, so PID attribution is the only reliable live binding.
let inferredPID = agentPIDFromHookEnvironment(agentName: def.name, env: env) ?? inferredAgentPID()
let hookEnvironmentPID = agentPIDFromHookEnvironment(agentName: def.name, env: env)
let inferredPID = hookEnvironmentPID ?? inferredAgentPID()
func resolvedAgentPID(mapped: ClaudeHookSessionRecord?) -> Int? {
// The wrapper exports the current agent process generation. It
// must supersede the durable record, whose PID belongs to the
// process that ran before a session resume.
hookEnvironmentPID ?? mapped?.pid ?? inferredPID
}
let hookWsFlag = optionValue(hookArgs, name: "--workspace")
let directWorkspaceArg = hookWsFlag ?? normalizedHookValue(env["CMUX_WORKSPACE_ID"])
let explicitSurfaceFlag = optionValue(hookArgs, name: "--surface")
Expand Down Expand Up @@ -30253,7 +30270,10 @@ export default CMUXSessionRestore;
func performAgentSessionTeardown() {
guard let mapped = sessionId.isEmpty ? nil : (try? store.lookup(sessionId: sessionId)) else { return }
sendAgentFeedTelemetry(workspaceId: mapped.workspaceId)
let suppressVisibleMutations = shouldSuppressNestedAgentVisibleMutations(currentAgentPID: mapped.pid, env: env)
let suppressVisibleMutations = shouldSuppressNestedAgentVisibleMutations(
currentAgentPID: resolvedAgentPID(mapped: mapped),
env: env
)
if suppressVisibleMutations {
telemetry.breadcrumb("\(def.name)-hook.session-end.nested-suppressed")
} else if let consumed = try? store.consume(sessionId: sessionId, workspaceId: nil, surfaceId: nil) {
Expand Down Expand Up @@ -30609,7 +30629,8 @@ export default CMUXSessionRestore;
displayName: def.displayName,
sessionId: sessionId,
cwd: preferredAgentHookResumeWorkingDirectory(kind: def.name, current: launchCommand, currentCwd: hookCwd, mapped: mapped),
launchCommand: resumeLaunchCommand
launchCommand: resumeLaunchCommand,
transcriptPath: input.transcriptPath ?? mapped?.transcriptPath
)
}
}
Expand Down Expand Up @@ -30642,7 +30663,7 @@ export default CMUXSessionRestore;
}
let workspaceId = target.workspaceId
let surfaceId = target.surfaceId
let pid = mapped?.pid ?? inferredPID
let pid = resolvedAgentPID(mapped: mapped)
let launchCommand = agentLaunchCommandFromEnvironment(env, fallbackPID: pid, fallbackKind: def.name, cwd: hookCwd ?? mapped?.cwd)
let transcriptPathForStore = input.transcriptPath ?? mapped?.transcriptPath
let resumeLaunchCommand = preferredAgentHookResumeLaunchCommand(
Expand Down Expand Up @@ -30675,7 +30696,8 @@ export default CMUXSessionRestore;
displayName: def.displayName,
sessionId: sessionId,
cwd: latest.cwd,
launchCommand: latest.launchCommand
launchCommand: latest.launchCommand,
transcriptPath: latest.transcriptPath
)
if let lifecycle = latest.agentLifecycle {
setAgentLifecycle(
Expand Down Expand Up @@ -30875,7 +30897,8 @@ export default CMUXSessionRestore;
displayName: def.displayName,
sessionId: sessionId,
cwd: preferredAgentHookResumeWorkingDirectory(kind: def.name, current: launchCommand, currentCwd: hookCwd, mapped: mapped),
launchCommand: resumeLaunchCommand
launchCommand: resumeLaunchCommand,
transcriptPath: transcriptPathForStore
)
if codexPromptTurnWentTerminal() {
stopStaleCodexPromptSubmit(restoreVisibleState: true)
Expand Down Expand Up @@ -30982,7 +31005,7 @@ export default CMUXSessionRestore;
let workspaceId = target.workspaceId
let surfaceId = target.surfaceId
sendAgentFeedTelemetry(workspaceId: workspaceId, surfaceId: surfaceId)
let pid = mapped?.pid ?? inferredPID
let pid = resolvedAgentPID(mapped: mapped)
let codexFailure: CodexHookFailureSummary?
let codexSubagentSignals: CodexTranscriptSubagentSignals
if def.name == "codex" {
Expand Down Expand Up @@ -31149,7 +31172,8 @@ export default CMUXSessionRestore;
displayName: def.displayName,
sessionId: sessionId,
cwd: cwd,
launchCommand: resumeLaunchCommand
launchCommand: resumeLaunchCommand,
transcriptPath: input.transcriptPath ?? mapped?.transcriptPath
)
}
if let pid, !suppressVisibleMutations {
Expand Down Expand Up @@ -31321,7 +31345,7 @@ export default CMUXSessionRestore;
let workspaceId = target.workspaceId
let surfaceId = target.surfaceId
sendAgentFeedTelemetryUnlessSuppressed(workspaceId: workspaceId, surfaceId: surfaceId)
let pid = mapped?.pid ?? inferredPID
let pid = resolvedAgentPID(mapped: mapped)
let launchCommand = agentLaunchCommandFromEnvironment(
env,
fallbackPID: pid,
Expand Down Expand Up @@ -31353,7 +31377,8 @@ export default CMUXSessionRestore;
displayName: def.displayName,
sessionId: sessionId,
cwd: preferredAgentHookResumeWorkingDirectory(kind: def.name, current: launchCommand, currentCwd: hookCwd, mapped: mapped),
launchCommand: resumeLaunchCommand
launchCommand: resumeLaunchCommand,
transcriptPath: input.transcriptPath ?? mapped?.transcriptPath
)
}
if let pid, !suppressVisibleMutations {
Expand Down Expand Up @@ -31517,7 +31542,7 @@ export default CMUXSessionRestore;
}

if !sessionId.isEmpty {
let pid = mapped?.pid ?? inferredPID
let pid = resolvedAgentPID(mapped: mapped)
let launchCommand = agentLaunchCommandFromEnvironment(
env,
fallbackPID: pid,
Expand Down Expand Up @@ -31695,7 +31720,7 @@ export default CMUXSessionRestore;
surfaceId: mapped.surfaceId,
cwd: hookCwd ?? mapped.cwd,
transcriptPath: input.transcriptPath ?? mapped.transcriptPath,
pid: mapped.pid,
pid: resolvedAgentPID(mapped: mapped),
launchCommand: mapped.launchCommand,
lastSubtitle: nil,
lastBody: nil,
Expand Down
Loading