@@ -3883,9 +3883,18 @@ impl CodexMessageProcessor {
38833883 self . command_exec_manager
38843884 . connection_closed ( connection_id)
38853885 . await ;
3886- self . thread_state_manager
3886+ let thread_ids_with_no_subscribers = self
3887+ . thread_state_manager
38873888 . remove_connection ( connection_id)
38883889 . await ;
3890+ for thread_id in thread_ids_with_no_subscribers {
3891+ let Ok ( thread) = self . thread_manager . get_thread ( thread_id) . await else {
3892+ self . finalize_thread_teardown ( thread_id) . await ;
3893+ continue ;
3894+ } ;
3895+ self . unload_thread_without_subscribers ( thread_id, thread)
3896+ . await ;
3897+ }
38893898 }
38903899
38913900 pub ( crate ) fn subscribe_running_assistant_turn_count ( & self ) -> watch:: Receiver < usize > {
@@ -5591,6 +5600,66 @@ impl CodexMessageProcessor {
55915600 . await ;
55925601 }
55935602
5603+ async fn unload_thread_without_subscribers (
5604+ & mut self ,
5605+ thread_id : ThreadId ,
5606+ thread : Arc < CodexThread > ,
5607+ ) {
5608+ // This connection was the last subscriber. Only now do we unload the thread.
5609+ info ! ( "thread {thread_id} has no subscribers; shutting down" ) ;
5610+ let should_start_unload_task = self . pending_thread_unloads . lock ( ) . await . insert ( thread_id) ;
5611+
5612+ // Any pending app-server -> client requests for this thread can no longer be
5613+ // answered; cancel their callbacks before shutdown/unload.
5614+ self . outgoing
5615+ . cancel_requests_for_thread ( thread_id, /*error*/ None )
5616+ . await ;
5617+ self . thread_state_manager
5618+ . remove_thread_state ( thread_id)
5619+ . await ;
5620+
5621+ if !should_start_unload_task {
5622+ return ;
5623+ }
5624+
5625+ let outgoing = self . outgoing . clone ( ) ;
5626+ let pending_thread_unloads = self . pending_thread_unloads . clone ( ) ;
5627+ let thread_manager = self . thread_manager . clone ( ) ;
5628+ let thread_watch_manager = self . thread_watch_manager . clone ( ) ;
5629+ tokio:: spawn ( async move {
5630+ match Self :: wait_for_thread_shutdown ( & thread) . await {
5631+ ThreadShutdownResult :: Complete => {
5632+ if thread_manager. remove_thread ( & thread_id) . await . is_none ( ) {
5633+ info ! ( "thread {thread_id} was already removed before teardown finalized" ) ;
5634+ thread_watch_manager
5635+ . remove_thread ( & thread_id. to_string ( ) )
5636+ . await ;
5637+ pending_thread_unloads. lock ( ) . await . remove ( & thread_id) ;
5638+ return ;
5639+ }
5640+ thread_watch_manager
5641+ . remove_thread ( & thread_id. to_string ( ) )
5642+ . await ;
5643+ let notification = ThreadClosedNotification {
5644+ thread_id : thread_id. to_string ( ) ,
5645+ } ;
5646+ outgoing
5647+ . send_server_notification ( ServerNotification :: ThreadClosed ( notification) )
5648+ . await ;
5649+ pending_thread_unloads. lock ( ) . await . remove ( & thread_id) ;
5650+ }
5651+ ThreadShutdownResult :: SubmitFailed => {
5652+ pending_thread_unloads. lock ( ) . await . remove ( & thread_id) ;
5653+ warn ! ( "failed to submit Shutdown to thread {thread_id}" ) ;
5654+ }
5655+ ThreadShutdownResult :: TimedOut => {
5656+ pending_thread_unloads. lock ( ) . await . remove ( & thread_id) ;
5657+ warn ! ( "thread {thread_id} shutdown timed out; leaving thread loaded" ) ;
5658+ }
5659+ }
5660+ } ) ;
5661+ }
5662+
55945663 async fn thread_unsubscribe (
55955664 & self ,
55965665 request_id : ConnectionRequestId ,
@@ -5638,58 +5707,8 @@ impl CodexMessageProcessor {
56385707 }
56395708
56405709 if !self . thread_state_manager . has_subscribers ( thread_id) . await {
5641- // This connection was the last subscriber. Only now do we unload the thread.
5642- info ! ( "thread {thread_id} has no subscribers; shutting down" ) ;
5643- self . pending_thread_unloads . lock ( ) . await . insert ( thread_id) ;
5644- // Any pending app-server -> client requests for this thread can no longer be
5645- // answered; cancel their callbacks before shutdown/unload.
5646- self . outgoing
5647- . cancel_requests_for_thread ( thread_id, /*error*/ None )
5710+ self . unload_thread_without_subscribers ( thread_id, thread)
56485711 . await ;
5649- self . thread_state_manager
5650- . remove_thread_state ( thread_id)
5651- . await ;
5652-
5653- let outgoing = self . outgoing . clone ( ) ;
5654- let pending_thread_unloads = self . pending_thread_unloads . clone ( ) ;
5655- let thread_manager = self . thread_manager . clone ( ) ;
5656- let thread_watch_manager = self . thread_watch_manager . clone ( ) ;
5657- tokio:: spawn ( async move {
5658- match Self :: wait_for_thread_shutdown ( & thread) . await {
5659- ThreadShutdownResult :: Complete => {
5660- if thread_manager. remove_thread ( & thread_id) . await . is_none ( ) {
5661- info ! (
5662- "thread {thread_id} was already removed before unsubscribe finalized"
5663- ) ;
5664- thread_watch_manager
5665- . remove_thread ( & thread_id. to_string ( ) )
5666- . await ;
5667- pending_thread_unloads. lock ( ) . await . remove ( & thread_id) ;
5668- return ;
5669- }
5670- thread_watch_manager
5671- . remove_thread ( & thread_id. to_string ( ) )
5672- . await ;
5673- let notification = ThreadClosedNotification {
5674- thread_id : thread_id. to_string ( ) ,
5675- } ;
5676- outgoing
5677- . send_server_notification ( ServerNotification :: ThreadClosed (
5678- notification,
5679- ) )
5680- . await ;
5681- pending_thread_unloads. lock ( ) . await . remove ( & thread_id) ;
5682- }
5683- ThreadShutdownResult :: SubmitFailed => {
5684- pending_thread_unloads. lock ( ) . await . remove ( & thread_id) ;
5685- warn ! ( "failed to submit Shutdown to thread {thread_id}" ) ;
5686- }
5687- ThreadShutdownResult :: TimedOut => {
5688- pending_thread_unloads. lock ( ) . await . remove ( & thread_id) ;
5689- warn ! ( "thread {thread_id} shutdown timed out; leaving thread loaded" ) ;
5690- }
5691- }
5692- } ) ;
56935712 }
56945713
56955714 self . outgoing
@@ -10047,7 +10066,8 @@ mod tests {
1004710066 state. lock ( ) . await . cancel_tx = Some ( cancel_tx) ;
1004810067 }
1004910068
10050- manager. remove_connection ( connection_a) . await ;
10069+ let threads_to_unload = manager. remove_connection ( connection_a) . await ;
10070+ assert_eq ! ( threads_to_unload, Vec :: <ThreadId >:: new( ) ) ;
1005110071 assert ! (
1005210072 tokio:: time:: timeout( Duration :: from_millis( 20 ) , & mut cancel_rx)
1005310073 . await
@@ -10068,7 +10088,8 @@ mod tests {
1006810088 let connection = ConnectionId ( 1 ) ;
1006910089
1007010090 manager. connection_initialized ( connection) . await ;
10071- manager. remove_connection ( connection) . await ;
10091+ let threads_to_unload = manager. remove_connection ( connection) . await ;
10092+ assert_eq ! ( threads_to_unload, Vec :: <ThreadId >:: new( ) ) ;
1007210093
1007310094 assert ! (
1007410095 manager
0 commit comments