@@ -78,6 +78,7 @@ final class ConversationController: ObservableObject {
7878 private var composerTransaction : ComposerTransaction ?
7979 private var shuttingDown = false
8080 private var retrying = false
81+ private var clientGeneration : UInt64 = 0
8182 private var startupUpdates : [ DesktopUpdate ] ?
8283 private var expectedRuntimeSessionID : String ?
8384 private var rosterPruneWorkItem : DispatchWorkItem ?
@@ -97,37 +98,40 @@ final class ConversationController: ObservableObject {
9798 self . client = client
9899 }
99100
101+ var reservedSessionID : String { conversation. sessionID ?? launchSessionID }
102+
100103 func start( ) { start ( force: false ) }
101104
102105 func retryIfNeeded( ) {
103- guard isRetryable, !retrying else { return }
106+ guard isRetryable, !retrying, !shuttingDown else { return }
104107 retrying = true
105108 isRetryable = false
106109 status = " Reconnecting… "
107- let previous = client
108- previous. close ( activeTurn: false ) { [ weak self] in
109- guard let self else { return }
110- self . client = previous. replacement ( )
111- self . retrying = false
112- self . start ( force: false )
113- }
110+ replaceClientAfterClosing ( force: false )
114111 }
115112
116113 private func start( force: Bool ) {
114+ guard !shuttingDown else { return }
117115 status = force ? " Recovering stale session lock… " : " Connecting… "
118116 let persisted = conversation. sessionID
117+ let activeClient = client
118+ let generation = clientGeneration
119119 startupUpdates = persisted == nil ? nil : [ ]
120- client . onUpdate = { [ weak self] update in
121- guard let self else { return }
120+ activeClient . onUpdate = { [ weak self, weak activeClient ] update in
121+ guard let self, let activeClient , self . isCurrentClient ( activeClient , generation : generation ) , ! self . shuttingDown else { return }
122122 if self . startupUpdates != nil { self . startupUpdates? . append ( update) }
123123 else { self . apply ( update) }
124124 }
125- client . onRuntimeEvent = { [ weak self] event in self ? . applyRuntime ( event) }
126- client . onDiagnostic = { [ weak self] text in
127- self ? . recordDiagnostic ( text , updateStatus : !text . hasPrefix ( " Ignored unsupported desktop update: " ) && !text . hasPrefix ( " Ignored malformed desktop update: " ) )
125+ activeClient . onRuntimeEvent = { [ weak self, weak activeClient ] event in
126+ guard let self, let activeClient , self . isCurrentClient ( activeClient , generation : generation ) , ! self . shuttingDown else { return }
127+ self . applyRuntime ( event )
128128 }
129- client. onExit = { [ weak self] code in
130- guard let self else { return }
129+ activeClient. onDiagnostic = { [ weak self, weak activeClient] text in
130+ guard let self, let activeClient, self . isCurrentClient ( activeClient, generation: generation) , !self . shuttingDown else { return }
131+ self . recordDiagnostic ( text, updateStatus: !text. hasPrefix ( " Ignored unsupported desktop update: " ) && !text. hasPrefix ( " Ignored malformed desktop update: " ) )
132+ }
133+ activeClient. onExit = { [ weak self, weak activeClient] code in
134+ guard let self, let activeClient, self . isCurrentClient ( activeClient, generation: generation) else { return }
131135 self . reduceSettlement ( . processExit( code) )
132136 var roster = self . agentRoster
133137 roster. retireActive ( at: Self . nowMilliseconds ( ) )
@@ -145,8 +149,8 @@ final class ConversationController: ObservableObject {
145149 reasoningEffort: inheritsConfig ? nil : conversation. reasoningEffort,
146150 force: force
147151 )
148- client . start ( options: options, loading: persisted != nil ) { [ weak self] result in
149- guard let self else { return }
152+ activeClient . start ( options: options, loading: persisted != nil ) { [ weak self, weak activeClient ] result in
153+ guard let self, let activeClient , self . isCurrentClient ( activeClient , generation : generation ) , ! self . shuttingDown else { return }
150154 switch result {
151155 case . failure( let error) :
152156 self . startupUpdates = nil
@@ -732,17 +736,28 @@ final class ConversationController: ObservableObject {
732736 }
733737
734738 private func retryAfterStaleLock( ) {
735- guard !retrying else { return }
739+ guard !retrying, !shuttingDown else { return }
736740 retrying = true
741+ replaceClientAfterClosing ( force: true )
742+ }
743+
744+ private func replaceClientAfterClosing( force: Bool ) {
737745 let previous = client
738- previous. close ( activeTurn: false ) { [ weak self] in
739- guard let self else { return }
746+ let generation = clientGeneration
747+ previous. close ( activeTurn: false ) { [ weak self, weak previous] in
748+ guard let self, let previous, !self . shuttingDown,
749+ self . isCurrentClient ( previous, generation: generation) else { return }
750+ self . clientGeneration &+= 1
740751 self . client = previous. replacement ( )
741752 self . retrying = false
742- self . start ( force: true )
753+ self . start ( force: force )
743754 }
744755 }
745756
757+ private func isCurrentClient( _ candidate: ACPClient , generation: UInt64 ) -> Bool {
758+ client === candidate && clientGeneration == generation
759+ }
760+
746761 private static func isStaleLockError( _ error: Error ) -> Bool {
747762 guard case ACPClientError . remote( _, let message) = error else { return false }
748763 return message. contains ( " use --force to override a stale lock " )
0 commit comments