Repository navigation
Fix Claude Code resume cwd and reused PID restore blocking #20249
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
Merged
+444
−40
Merged
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
d18879e
test: cover Claude transcript cwd selection
austinywang f1943a3
fix: resume Claude from transcript project cwd
austinywang f7464f0
test: cover reused legacy agent PIDs
austinywang 56f093d
fix: validate legacy agent PIDs by process identity
austinywang fe7bd71
Merge main (3cd046ae3c87) into agent-19306-claude-reboot-resume-54ea7f7c
austinywang 48e898c
Merge main into Claude resume fix
austinywang 965eb8b
Merge main (87d158116560) into agent-19306-claude-reboot-resume-54ea7f7c
austinywang 75a0b20
fix: unwrap candidate resume directories
austinywang 1c81691
fix: avoid ambiguous resume candidate closure
austinywang c9371ce
test: cover Claude resume review regressions
austinywang d8c2464
fix: address Claude resume review findings
austinywang 6b7540a
Merge main (b7d664ac49b3) into agent-19306-claude-reboot-resume-54ea7f7c
austinywang 703d79a
fix: use canonical Claude project slugs in restore
austinywang 047765a
test: reproduce process exit after restore identity capture
austinywang 985d202
test: avoid redundant process probes during restore
austinywang 9cc0f14
fix: prioritize absent PIDs and probe presence only when needed
austinywang fd573ff
test: retain the mismatched PID reclamation safety probe
austinywang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
.../macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Session/RestorableAgentProcessEvidence.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| public import CmuxFoundation | ||
|
|
||
| /// Process observations supplied to ``RestorableAgentProcessMatcher``. | ||
| public struct RestorableAgentProcessEvidence: Sendable { | ||
| /// The process ID from the restorable session record. | ||
| public let processID: Int | ||
| /// The process generation saved with the record, when available. | ||
| public let recordedIdentity: AgentPIDProcessIdentity? | ||
| /// The process generation observed during restore, when available. | ||
| public let currentIdentity: AgentPIDProcessIdentity? | ||
| /// Whether the process ID is currently occupied. | ||
| public let presence: RestorableAgentProcessPresence | ||
| /// Whether argv/environment identify the recorded cmux surface, when inspectable. | ||
| public let scopeMatches: Bool? | ||
| /// Whether argv/environment identify the expected agent session, when inspectable. | ||
| public let expectedProcessMatches: Bool? | ||
|
|
||
| /// Creates process evidence for one recorded process ID. | ||
| /// | ||
| /// - Parameters: | ||
| /// - processID: The PID saved in the session record. | ||
| /// - recordedIdentity: The saved generation, or `nil` for a legacy record. | ||
| /// - currentIdentity: The observed generation, or `nil` if unreadable. | ||
| /// - presence: Whether the PID is currently occupied. | ||
| /// - scopeMatches: Whether the process belongs to the saved surface, or `nil` if unreadable. | ||
| /// - expectedProcessMatches: Whether the agent and session match, or `nil` if unreadable. | ||
| public init( | ||
| processID: Int, | ||
| recordedIdentity: AgentPIDProcessIdentity?, | ||
| currentIdentity: AgentPIDProcessIdentity?, | ||
| presence: RestorableAgentProcessPresence, | ||
| scopeMatches: Bool?, | ||
| expectedProcessMatches: Bool? | ||
| ) { | ||
| self.processID = processID | ||
| self.recordedIdentity = recordedIdentity | ||
| self.currentIdentity = currentIdentity | ||
| self.presence = presence | ||
| self.scopeMatches = scopeMatches | ||
| self.expectedProcessMatches = expectedProcessMatches | ||
| } | ||
| } | ||
|
|
40 changes: 40 additions & 0 deletions
40
...s/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Session/RestorableAgentProcessMatcher.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| /// Classifies process evidence used by automatic agent-session restore. | ||
| public struct RestorableAgentProcessMatcher: Sendable { | ||
| /// Creates a stateless process matcher. | ||
| public init() {} | ||
|
|
||
| /// Returns whether the evidence authenticates the recorded process generation. | ||
| /// | ||
| /// A legacy record without a saved generation never authenticates a matching | ||
| /// process, but contradictory argv or scope evidence still proves that a reused | ||
| /// PID cannot block restoration. | ||
| /// | ||
| /// - Parameter evidence: Process observations for the saved PID. | ||
| /// - Returns: A match only for an authenticated generation, a mismatch for | ||
| /// definitive contradictory evidence, or unknown when observations are inconclusive. | ||
| public func match(_ evidence: RestorableAgentProcessEvidence) -> RestorableAgentProcessMatch { | ||
| if let recordedIdentity = evidence.recordedIdentity { | ||
| guard Int(recordedIdentity.pid) == evidence.processID else { | ||
| return evidence.presence == .absent ? .mismatches : .unknown | ||
| } | ||
| guard let currentIdentity = evidence.currentIdentity, | ||
| Int(currentIdentity.pid) == evidence.processID else { | ||
| return evidence.presence == .absent ? .mismatches : .unknown | ||
| } | ||
| guard currentIdentity == recordedIdentity else { | ||
| return .mismatches | ||
| } | ||
| } else if evidence.currentIdentity == nil, | ||
| evidence.presence == .absent { | ||
| return .mismatches | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| } | ||
|
|
||
| if evidence.scopeMatches == false || evidence.expectedProcessMatches == false { | ||
| return .mismatches | ||
| } | ||
| guard evidence.scopeMatches != nil, evidence.expectedProcessMatches != nil else { | ||
| return evidence.presence == .absent ? .mismatches : .unknown | ||
| } | ||
| return evidence.recordedIdentity == nil ? .unknown : .matches | ||
| } | ||
| } | ||
10 changes: 10 additions & 0 deletions
10
.../macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Session/RestorableAgentProcessPresence.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| /// Tri-state evidence about whether a recorded process ID is still occupied. | ||
| public enum RestorableAgentProcessPresence: Equatable, Hashable, Sendable { | ||
| /// The process ID currently exists. | ||
| case present | ||
| /// The process ID is confirmed absent. | ||
| case absent | ||
| /// The process table could not establish whether the ID exists. | ||
| case unknown | ||
| } | ||
|
|
104 changes: 104 additions & 0 deletions
104
...CmuxWorkspaces/Tests/CmuxWorkspacesTests/Session/RestorableAgentProcessMatcherTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import CmuxFoundation | ||
| import Testing | ||
| @testable import CmuxWorkspaces | ||
|
|
||
| @Suite("RestorableAgentProcessMatcher") | ||
| struct RestorableAgentProcessMatcherTests { | ||
| private let matcher = RestorableAgentProcessMatcher() | ||
|
|
||
| @Test("A reused legacy PID with contradictory process evidence mismatches", arguments: [false, true]) | ||
| func reusedLegacyPIDMismatches(hasCurrentIdentity: Bool) { | ||
| let result = matcher.match(.init( | ||
| processID: 42, | ||
| recordedIdentity: nil, | ||
| currentIdentity: hasCurrentIdentity ? identity() : nil, | ||
| presence: .present, | ||
| scopeMatches: true, | ||
| expectedProcessMatches: false | ||
| )) | ||
| #expect(result == .mismatches) | ||
| } | ||
|
|
||
| @Test("A different surface proves a legacy PID is reused even without agent details") | ||
| func reusedLegacyPIDInAnotherScopeMismatches() { | ||
| #expect(matcher.match(.init( | ||
| processID: 42, | ||
| recordedIdentity: nil, | ||
| currentIdentity: nil, | ||
| presence: .present, | ||
| scopeMatches: false, | ||
| expectedProcessMatches: nil | ||
| )) == .mismatches) | ||
| } | ||
|
|
||
| @Test("A matching legacy PID remains unknown without a saved generation", arguments: [false, true]) | ||
| func matchingLegacyPIDRemainsUnknown(hasCurrentIdentity: Bool) { | ||
| let result = matcher.match(.init( | ||
| processID: 42, | ||
| recordedIdentity: nil, | ||
| currentIdentity: hasCurrentIdentity ? identity() : nil, | ||
| presence: .present, | ||
| scopeMatches: true, | ||
| expectedProcessMatches: true | ||
| )) | ||
| #expect(result == .unknown) | ||
| } | ||
|
|
||
| @Test("A matching saved process generation authenticates") | ||
| func matchingSavedGenerationAuthenticates() { | ||
| let identity = identity() | ||
| let result = matcher.match(.init( | ||
| processID: 42, | ||
| recordedIdentity: identity, | ||
| currentIdentity: identity, | ||
| presence: .present, | ||
| scopeMatches: true, | ||
| expectedProcessMatches: true | ||
| )) | ||
| #expect(result == .matches) | ||
| } | ||
|
|
||
| @Test("A changed process generation proves PID reuse even when argv is unreadable") | ||
| func changedGenerationMismatches() { | ||
| #expect(matcher.match(.init( | ||
| processID: 42, | ||
| recordedIdentity: identity(), | ||
| currentIdentity: identity(startSeconds: 20), | ||
| presence: .present, | ||
| scopeMatches: nil, | ||
| expectedProcessMatches: nil | ||
| )) == .mismatches) | ||
| } | ||
|
|
||
| @Test("Unreadable processes remain uncertain unless their absence is definitive", | ||
| arguments: [RestorableAgentProcessPresence.present, .absent, .unknown], [false, true]) | ||
| func unreadableProcessRequiresDefinitiveAbsence( | ||
| presence: RestorableAgentProcessPresence, | ||
| hasRecordedIdentity: Bool | ||
| ) { | ||
| #expect(matcher.match(.init( | ||
| processID: 42, | ||
| recordedIdentity: hasRecordedIdentity ? identity() : nil, | ||
| currentIdentity: nil, | ||
| presence: presence, | ||
| scopeMatches: nil, | ||
| expectedProcessMatches: nil | ||
| )) == (presence == .absent ? .mismatches : .unknown)) | ||
| } | ||
|
|
||
| @Test("A saved generation cannot authenticate without a matching current identity") | ||
| func missingCurrentGenerationStaysUnknown() { | ||
| #expect(matcher.match(.init( | ||
| processID: 42, | ||
| recordedIdentity: identity(), | ||
| currentIdentity: nil, | ||
| presence: .present, | ||
| scopeMatches: true, | ||
| expectedProcessMatches: true | ||
| )) == .unknown) | ||
| } | ||
|
|
||
| private func identity(startSeconds: Int64 = 10) -> AgentPIDProcessIdentity { | ||
| AgentPIDProcessIdentity(pid: 42, startSeconds: startSeconds, startMicroseconds: 2) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.