Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
105 changes: 104 additions & 1 deletion CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,13 @@ private final class ClaudeHookSessionStore {
}
}

private let codexHookWrapperProcessNames: Set<String> = [
"sh",
"bash",
"zsh",
"env"
]

enum CLIIDFormat: String {
case refs
case uuids
Expand Down Expand Up @@ -12763,12 +12770,21 @@ struct CMUXCLI {
workspaceId: workspaceId,
client: client
)
let agentPIDKey = codexAgentPIDKey(sessionId: parsedInput.sessionId)
let codexPid = inferredCodexAgentPID()
if let sessionId = parsedInput.sessionId {
try? sessionStore.upsert(
sessionId: sessionId,
workspaceId: workspaceId,
surfaceId: surfaceId,
cwd: parsedInput.cwd
cwd: parsedInput.cwd,
pid: codexPid
)
}
if let codexPid {
_ = try? sendV1Command(
"set_agent_pid \(agentPIDKey) \(codexPid) --tab=\(workspaceId)",
client: client
)
}
print("{}")
Expand All @@ -12781,6 +12797,23 @@ struct CMUXCLI {
fallback: workspaceArg,
client: client
)
let agentPIDKey = codexAgentPIDKey(sessionId: parsedInput.sessionId ?? mappedSession?.sessionId)
let codexPid = mappedSession?.pid ?? inferredCodexAgentPID()
if let sessionId = parsedInput.sessionId, let mappedSession {
try? sessionStore.upsert(
sessionId: sessionId,
workspaceId: workspaceId,
surfaceId: mappedSession.surfaceId,
cwd: parsedInput.cwd ?? mappedSession.cwd,
pid: codexPid
)
Comment on lines +13303 to +13310
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Persist the recovered session on prompt-submit.

Line 12802 only upserts when a prior session record already exists. If SessionStart was skipped or the store entry was lost, this path still recovers/registers a PID, but none of that recovered state is saved, so stop has to rediscover everything from ambient state again.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 12802 - 12809, The current prompt-submit path
only calls sessionStore.upsert when mappedSession exists so recovered PID/state
isn't persisted; modify the logic in the prompt-submit handler to always call
sessionStore.upsert (use sessionId from parsedInput and codexPid) even when
mappedSession is nil: supply surfaceId as mappedSession?.surfaceId or a sensible
default, cwd as parsedInput.cwd ?? mappedSession?.cwd, and workspaceId as
available; ensure the upsert invocation (sessionStore.upsert) runs for both
branches so recovered sessions are saved for later stop handling.

}
if let codexPid {
_ = try? sendV1Command(
"set_agent_pid \(agentPIDKey) \(codexPid) --tab=\(workspaceId)",
client: client
)
}
_ = try? sendV1Command("clear_notifications --tab=\(workspaceId)", client: client)
try setCodexStatus(
client: client,
Expand All @@ -12806,11 +12839,13 @@ struct CMUXCLI {
workspaceId: workspaceId,
client: client
)
let agentPIDKey = codexAgentPIDKey(sessionId: parsedInput.sessionId ?? mappedSession?.sessionId)

// Build completion notification from Codex stop payload
let lastMessage = parsedInput.object?["last_assistant_message"] as? String
?? parsedInput.object?["lastAssistantMessage"] as? String
let cwd = parsedInput.cwd ?? mappedSession?.cwd
let codexPid = mappedSession?.pid ?? inferredCodexAgentPID()
let projectName: String? = {
guard let cwd, !cwd.isEmpty else { return nil }
return URL(fileURLWithPath: NSString(string: cwd).expandingTildeInPath).lastPathComponent
Expand All @@ -12822,10 +12857,17 @@ struct CMUXCLI {
workspaceId: workspaceId,
surfaceId: surfaceId,
cwd: cwd,
pid: codexPid,
lastSubtitle: "Completed",
lastBody: lastMessage.map { truncate($0, maxLength: 200) }
)
}
if let codexPid {
_ = try? sendV1Command(
"set_agent_pid \(agentPIDKey) \(codexPid) --tab=\(workspaceId)",
client: client
)
}

// Send completion notification
var subtitle = "Completed"
Expand Down Expand Up @@ -12875,6 +12917,67 @@ struct CMUXCLI {
_ = try client.send(command: cmd)
}

private func codexAgentPIDKey(sessionId: String?) -> String {
guard let sessionId = sessionId?.trimmingCharacters(in: .whitespacesAndNewlines),
!sessionId.isEmpty else {
return "codex"
}
return "codex.\(sessionId)"
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 3, 2026

Choose a reason for hiding this comment

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

P2: Sanitize sessionId before building agentPIDKey; raw IDs can break set_agent_pid command tokenization.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At CLI/cmux.swift, line 12925:

<comment>Sanitize `sessionId` before building `agentPIDKey`; raw IDs can break `set_agent_pid` command tokenization.</comment>

<file context>
@@ -12875,6 +12917,67 @@ struct CMUXCLI {
+              !sessionId.isEmpty else {
+            return "codex"
+        }
+        return "codex.\(sessionId)"
+    }
+
</file context>
Fix with Cubic

}

private func inferredCodexAgentPID() -> Int? {
var candidate = getppid()
var remainingWrapperSkips = 8

while candidate > 1, remainingWrapperSkips > 0 {
guard let processName = processName(for: candidate) else { break }
if !codexHookWrapperProcessNames.contains(processName) {
break
}
let next = parentPID(of: candidate)
guard next > 1, next != candidate else { break }
candidate = next
Comment on lines +13433 to +13440
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# First, let's check if the file exists and read the specific lines
wc -l CLI/cmux.swift

Repository: manaflow-ai/cmux

Length of output: 80


🏁 Script executed:

#!/bin/bash

# Read lines 12932-12939 and surrounding context (12920-12950) for better understanding
sed -n '12920,12950p' CLI/cmux.swift

Repository: manaflow-ai/cmux

Length of output: 1214


🏁 Script executed:

#!/bin/bash

# Search for the definition of codexHookWrapperProcessNames
rg "codexHookWrapperProcessNames" CLI/cmux.swift -B 2 -A 2

Repository: manaflow-ai/cmux

Length of output: 386


🏁 Script executed:

#!/bin/bash

# Search for how the wrapper detection logic is used and the rescan mechanism
rg "parentPID\|remainingWrapperSkips" CLI/cmux.swift -B 3 -A 3

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

#!/bin/bash

# Get the full definition of codexHookWrapperProcessNames
rg "private let codexHookWrapperProcessNames" CLI/cmux.swift -A 10

Repository: manaflow-ai/cmux

Length of output: 233


🏁 Script executed:

#!/bin/bash

# Search for hook payload or environment variable usage for PID
rg "CODEX_HOOK|hook.*PID|payload.*PID" CLI/cmux.swift -i

Repository: manaflow-ai/cmux

Length of output: 781


🏁 Script executed:

#!/bin/bash

# Search for how inferredCodexAgentPID is used and whether there are other PID detection mechanisms
rg "inferredCodexAgentPID\|codexAgentPID" CLI/cmux.swift -B 2 -A 2 | head -60

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

#!/bin/bash

# Check for any normalization or variant handling of process names
rg "processName|sh|bash|zsh|env" CLI/cmux.swift | grep -E "(normalize|variant|strip|replace)" | head -20

Repository: manaflow-ai/cmux

Length of output: 1491


Wrapper detection is too narrow and needs broadening or alternative strategy.

The hard-coded codexHookWrapperProcessNames set contains only ["sh", "bash", "zsh", "env"]. Line 12934 exits the parent-chain traversal as soon as any process name falls outside this set. If Codex runs through wrapper shells not in this list (e.g., ksh, tcsh, fish, or non-standard login-shell variants), the inferred PID will stop prematurely at the wrapper rather than the Codex root. This causes the periodic rescan to drop tracking as soon as that short-lived wrapper exits.

Either:

  1. Normalize process names (strip version suffixes, resolve symlinks) and broaden the detection set, or
  2. Extract and prefer an explicit PID from hook payload/environment when available (no such mechanism currently exists).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLI/cmux.swift` around lines 12932 - 12939, The parent-chain traversal in the
loop using codexHookWrapperProcessNames is too narrow and breaks when
encountering shells not listed; update detection in the loop that uses
processName(for:) and parentPID(of:) to broaden or normalize matches: either
expand codexHookWrapperProcessNames to include additional common shells (ksh,
tcsh, fish, etc.) and normalize processName(for:) results by stripping
version/suffixes and resolving symlinks, or change the logic to prefer an
explicit PID from the hook payload/environment when present (add a check before
the while using the hook-provided PID), and allow a configurable wrapper
whitelist rather than hard-coded names so remainingWrapperSkips and candidate
traversal correctly climb past transient wrappers.

remainingWrapperSkips -= 1
}

return candidate > 1 ? Int(candidate) : nil
}

private func parentPID(of pid: pid_t) -> pid_t {
var info = kinfo_proc()
var size = MemoryLayout<kinfo_proc>.size
var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid]
guard sysctl(&mib, 4, &info, &size, nil, 0) == 0 else {
return -1
}
return info.kp_eproc.e_ppid
}

private func processName(for pid: pid_t) -> String? {
let process = Process()
let stdout = Pipe()
process.executableURL = URL(fileURLWithPath: "/bin/ps")
process.arguments = ["-p", String(pid), "-o", "comm="]
process.standardOutput = stdout
process.standardError = FileHandle.nullDevice

do {
try process.run()
} catch {
return nil
}
process.waitUntilExit()
guard process.terminationStatus == 0 else { return nil }

let data = stdout.fileHandleForReading.readDataToEndOfFile()
guard let output = String(data: data, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines),
!output.isEmpty else {
return nil
}
return URL(fileURLWithPath: output).lastPathComponent.lowercased()
}

private func versionSummary() -> String {
let info = resolvedVersionInfo()
let commit = info["CMUXCommit"].flatMap { normalizedCommitHash($0) }
Expand Down
Loading