-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Fix Claude failed resume clobbering surface restore #6014
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22158,6 +22158,15 @@ struct CMUXCLI { | |
| ) | ||
| let shouldPromoteActiveSession = !isForkSessionLaunch && (isClearSessionStart || canReplaceStoppedSession) | ||
| if let sessionId = parsedInput.sessionId, !isForkSessionLaunch { | ||
| let mappedSession = try? sessionStore.lookup(sessionId: sessionId) | ||
| let canPublishResumeBinding = shouldPublishClaudeResumeBinding( | ||
| sessionId: sessionId, | ||
| parsedInput: parsedInput, | ||
| mappedSession: mappedSession, | ||
| env: ProcessInfo.processInfo.environment, | ||
| fallbackPID: claudePid | ||
| ) | ||
| let shouldPromoteThisSession = shouldPromoteActiveSession && canPublishResumeBinding | ||
| // Non-clear SessionStart can arrive late from startup/resume/compact | ||
| // after /clear, so only /clear or replacement of a stopped owner | ||
| // establishes a new active boundary. | ||
|
|
@@ -22170,11 +22179,11 @@ struct CMUXCLI { | |
| pid: claudePid, | ||
| launchCommand: launchCommand, | ||
| isRestorable: false, | ||
| agentLifecycle: shouldPromoteActiveSession ? .running : .unknown, | ||
| markActive: shouldPromoteActiveSession, | ||
| agentLifecycle: shouldPromoteThisSession ? .running : .unknown, | ||
| markActive: shouldPromoteThisSession, | ||
| turnId: parsedInput.turnId | ||
| ) | ||
| if shouldPromoteActiveSession { | ||
| if shouldPromoteThisSession { | ||
| publishAgentSurfaceResumeBinding( | ||
| client: client, | ||
| workspaceId: workspaceId, | ||
|
|
@@ -22283,29 +22292,38 @@ struct CMUXCLI { | |
| sessionRecord: mappedSession | ||
| ) | ||
| if let sessionId = parsedInput.sessionId { | ||
| let canPublishResumeBinding = shouldPublishClaudeResumeBinding( | ||
| sessionId: sessionId, | ||
| parsedInput: parsedInput, | ||
| mappedSession: mappedSession, | ||
| env: ProcessInfo.processInfo.environment, | ||
| fallbackPID: claudePid | ||
| ) | ||
| try? sessionStore.upsert( | ||
| sessionId: sessionId, | ||
| workspaceId: workspaceId, | ||
| surfaceId: surfaceId, | ||
| cwd: parsedInput.cwd, | ||
| transcriptPath: parsedInput.transcriptPath, | ||
| isRestorable: true, | ||
| isRestorable: canPublishResumeBinding, | ||
| agentLifecycle: .idle, | ||
| lastSubtitle: completion?.subtitle, | ||
| lastBody: completion?.body, | ||
| markActive: true, | ||
| allowsNewSessionReplacement: true | ||
| ) | ||
| publishAgentSurfaceResumeBinding( | ||
| client: client, | ||
| workspaceId: workspaceId, | ||
| surfaceId: surfaceId, | ||
| kind: "claude", | ||
| displayName: String(localized: "cli.claude-hook.notification.title", defaultValue: "Claude Code"), | ||
| sessionId: sessionId, | ||
| cwd: parsedInput.cwd ?? mappedSession?.cwd, | ||
| launchCommand: mappedSession?.launchCommand | ||
| ) | ||
| if canPublishResumeBinding { | ||
| publishAgentSurfaceResumeBinding( | ||
| client: client, | ||
| workspaceId: workspaceId, | ||
| surfaceId: surfaceId, | ||
| kind: "claude", | ||
| displayName: String(localized: "cli.claude-hook.notification.title", defaultValue: "Claude Code"), | ||
| sessionId: sessionId, | ||
| cwd: parsedInput.cwd ?? mappedSession?.cwd, | ||
| launchCommand: mappedSession?.launchCommand | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| setAgentLifecycle( | ||
|
|
@@ -22402,6 +22420,13 @@ struct CMUXCLI { | |
| cwd: parsedInput.cwd | ||
| ) | ||
| : nil | ||
| let canPublishResumeBinding = shouldPublishClaudeResumeBinding( | ||
| sessionId: sessionId, | ||
| parsedInput: parsedInput, | ||
| mappedSession: mappedSession, | ||
| env: ProcessInfo.processInfo.environment, | ||
| fallbackPID: claudePid | ||
| ) | ||
| try? sessionStore.upsert( | ||
| sessionId: sessionId, | ||
| workspaceId: workspaceId, | ||
|
|
@@ -22410,21 +22435,23 @@ struct CMUXCLI { | |
| transcriptPath: parsedInput.transcriptPath, | ||
| pid: mappedSession == nil ? claudePid : nil, | ||
| launchCommand: firstSightingLaunchCommand, | ||
| isRestorable: true, | ||
| isRestorable: canPublishResumeBinding, | ||
| agentLifecycle: .running, | ||
| markActive: true, | ||
| turnId: parsedInput.turnId | ||
| ) | ||
| publishAgentSurfaceResumeBinding( | ||
| client: client, | ||
| workspaceId: workspaceId, | ||
| surfaceId: surfaceId, | ||
| kind: "claude", | ||
| displayName: String(localized: "cli.claude-hook.notification.title", defaultValue: "Claude Code"), | ||
| sessionId: sessionId, | ||
| cwd: parsedInput.cwd ?? mappedSession?.cwd, | ||
| launchCommand: mappedSession?.launchCommand ?? firstSightingLaunchCommand | ||
| ) | ||
| if canPublishResumeBinding { | ||
| publishAgentSurfaceResumeBinding( | ||
| client: client, | ||
| workspaceId: workspaceId, | ||
| surfaceId: surfaceId, | ||
| kind: "claude", | ||
| displayName: String(localized: "cli.claude-hook.notification.title", defaultValue: "Claude Code"), | ||
| sessionId: sessionId, | ||
| cwd: parsedInput.cwd ?? mappedSession?.cwd, | ||
| launchCommand: mappedSession?.launchCommand ?? firstSightingLaunchCommand | ||
| ) | ||
| } | ||
| } | ||
| _ = try sendV1Command("clear_notifications --tab=\(workspaceId)", client: client) | ||
| setAgentLifecycle( | ||
|
|
@@ -22944,6 +22971,40 @@ struct CMUXCLI { | |
| } | ||
| } | ||
|
|
||
| private func shouldPublishClaudeResumeBinding( | ||
| sessionId: String, | ||
| parsedInput: ClaudeHookParsedInput, | ||
| mappedSession: ClaudeHookSessionRecord?, | ||
| env: [String: String], | ||
| fallbackPID: Int? | ||
| ) -> Bool { | ||
| guard let normalizedSessionId = normalizedHookValue(sessionId) else { return false } | ||
| if normalizedHookValue(parsedInput.transcriptPath ?? mappedSession?.transcriptPath) != nil { | ||
| return true | ||
| } | ||
| if let resumedSessionId = claudeResumeLaunchSessionId(env: env, fallbackPID: fallbackPID), | ||
| resumedSessionId == normalizedSessionId { | ||
| return false | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| private func claudeResumeLaunchSessionId(env: [String: String], fallbackPID: Int?) -> String? { | ||
| guard let arguments = claudeRawLaunchArguments(env: env, fallbackPID: fallbackPID) else { | ||
| return nil | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| for (index, argument) in arguments.enumerated() { | ||
| if argument == "--resume" || argument == "-r" { | ||
| guard index + 1 < arguments.count else { return nil } | ||
| return normalizedHookValue(arguments[index + 1]) | ||
| } | ||
| for prefix in ["--resume=", "-r="] where argument.hasPrefix(prefix) { | ||
| return normalizedHookValue(String(argument.dropFirst(prefix.count))) | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
Comment on lines
+22974
to
+23006
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor 🧩 Analysis chain🌐 Web query:
💡 Result: The Claude CLI does not support passing multiple --resume flags in a single command. The --resume flag is designed to accept a single argument—either a specific session ID, a session name, or no argument at all to trigger an interactive session picker [1][2]. If you attempt to provide multiple --resume flags, it is not a supported use case, and the behavior is not explicitly defined in the official documentation [1][2]. To resume a session, you should use the flag once, followed by the desired session identifier [1][2]: claude --resume If you need to switch between sessions, you can use the interactive /resume picker within an active session or run the command without arguments to open the picker [2]. Additionally, you can use --continue to resume the most recent session in the current directory [2]. Citations: 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate the target file
ls -la
# Search for CLI/cmux.swift existence
if [ -f "CLI/cmux.swift" ]; then
echo "FOUND: CLI/cmux.swift"
else
echo "NOT FOUND: CLI/cmux.swift; searching..."
fd -a "cmux.swift" -t f
fi
# Print relevant regions for resume parsing logic
rg -n --no-heading "claudeResumeLaunchSessionId|--resume|\\-r\\b" CLI/cmux.swift || true
# Show surrounding code for the function
if [ -f "CLI/cmux.swift" ]; then
line=$(rg -n "claudeResumeLaunchSessionId" CLI/cmux.swift | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
start=$((line-80))
end=$((line+220))
echo "---- Function region (approx) lines ${start}-${end} ----"
nl -ba CLI/cmux.swift | sed -n "${start},${end}p"
fi
echo "---- All nearby resume parsing blocks (context) ----"
rg -n --context 6 --no-heading "if argument == \"--resume\" \\|\\| argument == \"-r\"|hasPrefix\\(\"--resume=\"\\)|hasPrefix\\(\"-r=\"\\)|\\[\"--resume=\", \"-r=\"\\]" CLI/cmux.swift || true
fiRepository: manaflow-ai/cmux Length of output: 6963 🏁 Script executed: #!/bin/bash
set -euo pipefail
if [ -f "CLI/cmux.swift" ]; then
echo "FOUND: CLI/cmux.swift"
else
echo "NOT FOUND: CLI/cmux.swift"
fd -a -t f -e swift "cmux.swift"
fi
if [ -f "CLI/cmux.swift" ]; then
rg -n --no-heading "claudeResumeLaunchSessionId|--resume|-r\\b" CLI/cmux.swift || true
line=$(rg -n "claudeResumeLaunchSessionId" CLI/cmux.swift | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
start=$((line-80))
end=$((line+260))
echo "---- Function region (approx) lines ${start}-${end} ----"
nl -ba CLI/cmux.swift | sed -n "${start},${end}p"
fi
echo "---- Candidate resume parsing blocks with context ----"
rg -n --context 8 --no-heading "resume|--resume|\\-r\\b|normalizedHookValue" CLI/cmux.swift | head -n 200
fiRepository: manaflow-ai/cmux Length of output: 1651 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Verify file and inspect relevant resume parsing blocks in CLI/cmux.swift
if [ -f "CLI/cmux.swift" ]; then
echo "FOUND: CLI/cmux.swift"
else
echo "NOT FOUND: CLI/cmux.swift"
fd -a -t f "cmux.swift"
exit 0
fi
echo "---- resume-related matches ----"
rg -n --no-heading "--resume|\\-r\\b|claudeResumeLaunchSessionId|normalizedHookValue" CLI/cmux.swift
echo "---- showing function claudeResumeLaunchSessionId (approx) ----"
fn_line=$(rg -n --no-heading "claudeResumeLaunchSessionId" CLI/cmux.swift | head -n1 | cut -d: -f1)
start=$((fn_line-60))
end=$((fn_line+320))
nl -ba CLI/cmux.swift | sed -n "${start},${end}p" | sed -n '1,260p'
echo "---- showing any other occurrences of '--resume'/'-r' parsing (context) ----"
rg -n --context 12 --no-heading "if argument == \"--resume\" \\|\\| argument == \"-r\"|hasPrefix\\(\"--resume=\"\\)|hasPrefix\\(\"-r=\"\\)" CLI/cmux.swiftRepository: manaflow-ai/cmux Length of output: 598 Use first
🤖 Prompt for AI Agents |
||
|
|
||
| private func isClaudeClearSessionStart(_ parsedInput: ClaudeHookParsedInput) -> Bool { | ||
| guard let source = parsedInput.object?["source"] as? String else { | ||
| return false | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
claudeResumeLaunchSessionIdreturnsnilifCMUX_AGENT_LAUNCH_ARGV_B64is absent andprocessArguments(for:)cannot read the PID (e.g.,claudePidis nil). When both fallbacks fail, the code skips the resume-id comparison and falls through toreturn true, publishing the binding for a failed resume — the exact outcome the PR is trying to prevent. The bug is only fully suppressed when at least one argv source is reliably available on the hook invocation path.