@@ -58,6 +58,9 @@ struct LiveSession {
5858 /// the old session's `tx` just before supersede could still enqueue a
5959 /// `RelayOpen` onto the stale stream and sit until the relay timeout.
6060 shutdown : oneshot:: Sender < ( ) > ,
61+ /// Set after the supervisor confirms that every expected foreground
62+ /// attachment has closed and terminal output delivery is complete.
63+ terminal_delivery_finalized : bool ,
6164 #[ allow( dead_code) ]
6265 connected_at : Instant ,
6366}
@@ -125,6 +128,7 @@ impl SupervisorSessionRegistry {
125128 session_id,
126129 tx,
127130 shutdown,
131+ terminal_delivery_finalized : false ,
128132 connected_at : Instant :: now ( ) ,
129133 } ,
130134 ) ;
@@ -163,15 +167,17 @@ impl SupervisorSessionRegistry {
163167 /// This guards against the supersede race: an old session's task may
164168 /// finish long after a new session has taken its place. The old task's
165169 /// cleanup must not evict the new registration.
166- fn remove_if_current ( & self , sandbox_id : & str , session_id : & str ) -> bool {
170+ fn remove_if_current ( & self , sandbox_id : & str , session_id : & str ) -> Option < bool > {
167171 let mut sessions = self . sessions . lock ( ) . unwrap ( ) ;
168172 let is_current = sessions
169173 . get ( sandbox_id)
170174 . is_some_and ( |s| s. session_id == session_id) ;
171175 if is_current {
172- sessions. remove ( sandbox_id) ;
176+ return sessions
177+ . remove ( sandbox_id)
178+ . map ( |session| session. terminal_delivery_finalized ) ;
173179 }
174- is_current
180+ None
175181 }
176182
177183 /// Look up the sender for a supervisor session, waiting up to `timeout`
@@ -210,6 +216,23 @@ impl SupervisorSessionRegistry {
210216 self . sessions . lock ( ) . unwrap ( ) . contains_key ( sandbox_id)
211217 }
212218
219+ pub fn terminal_delivery_finalized ( & self , sandbox_id : & str ) -> bool {
220+ self . sessions
221+ . lock ( )
222+ . unwrap ( )
223+ . get ( sandbox_id)
224+ . is_some_and ( |session| session. terminal_delivery_finalized )
225+ }
226+
227+ pub fn finalize_main_process_exit ( & self , sandbox_id : & str ) -> bool {
228+ let mut sessions = self . sessions . lock ( ) . unwrap ( ) ;
229+ let Some ( session) = sessions. get_mut ( sandbox_id) else {
230+ return false ;
231+ } ;
232+ session. terminal_delivery_finalized = true ;
233+ true
234+ }
235+
213236 pub fn is_current_session ( & self , sandbox_id : & str , session_id : & str ) -> bool {
214237 self . sessions
215238 . lock ( )
@@ -797,17 +820,17 @@ pub async fn handle_connect_supervisor(
797820 shutdown_rx,
798821 )
799822 . await ;
800- let still_ours = state_clone
823+ let terminal_finalized = state_clone
801824 . supervisor_sessions
802825 . remove_if_current ( & sandbox_id_clone, & session_id) ;
803- if still_ours {
826+ if let Some ( terminal_finalized ) = terminal_finalized {
804827 info ! ( sandbox_id = %sandbox_id_clone, session_id = %session_id, "supervisor session: ended" ) ;
805828 state_clone
806829 . telemetry
807830 . sandbox_session_disconnected ( & sandbox_id_clone) ;
808831 if let Err ( err) = state_clone
809832 . compute
810- . supervisor_session_disconnected ( & sandbox_id_clone)
833+ . supervisor_session_disconnected ( & sandbox_id_clone, terminal_finalized )
811834 . await
812835 {
813836 warn ! (
@@ -874,6 +897,14 @@ pub async fn handle_finalize_main_process_exit(
874897 . finalize_main_process_exit ( & report. sandbox_id , & report. instance_id )
875898 . await
876899 . map_err ( Status :: failed_precondition) ?;
900+ if !state
901+ . supervisor_sessions
902+ . finalize_main_process_exit ( & report. sandbox_id )
903+ {
904+ return Err ( Status :: failed_precondition (
905+ "supervisor session is not connected" ,
906+ ) ) ;
907+ }
877908 Ok ( Response :: new (
878909 openshell_core:: proto:: FinalizeMainProcessExitResponse { } ,
879910 ) )
@@ -1133,7 +1164,7 @@ mod tests {
11331164 let ( tx, _rx) = mpsc:: channel ( 1 ) ;
11341165 registry. register ( "sbx" . to_string ( ) , "s1" . to_string ( ) , tx, make_shutdown ( ) ) ;
11351166
1136- assert ! ( registry. remove_if_current( "sbx" , "s1" ) ) ;
1167+ assert_eq ! ( registry. remove_if_current( "sbx" , "s1" ) , Some ( false ) ) ;
11371168 assert ! ( !registry. sessions. lock( ) . unwrap( ) . contains_key( "sbx" ) ) ;
11381169 }
11391170
@@ -1159,7 +1190,7 @@ mod tests {
11591190
11601191 // Cleanup from the old session task runs late. It must NOT evict the
11611192 // newly registered session.
1162- assert ! ( ! registry. remove_if_current( "sbx" , "s-old" ) ) ;
1193+ assert_eq ! ( registry. remove_if_current( "sbx" , "s-old" ) , None ) ;
11631194 let sessions = registry. sessions . lock ( ) . unwrap ( ) ;
11641195 assert ! (
11651196 sessions. contains_key( "sbx" ) ,
@@ -1171,7 +1202,18 @@ mod tests {
11711202 #[ test]
11721203 fn remove_if_current_unknown_sandbox_is_noop ( ) {
11731204 let registry = SupervisorSessionRegistry :: new ( ) ;
1174- assert ! ( !registry. remove_if_current( "sbx-does-not-exist" , "s1" ) ) ;
1205+ assert_eq ! ( registry. remove_if_current( "sbx-does-not-exist" , "s1" ) , None ) ;
1206+ }
1207+
1208+ #[ test]
1209+ fn remove_if_current_returns_terminal_finalization_state ( ) {
1210+ let registry = SupervisorSessionRegistry :: new ( ) ;
1211+ let ( tx, _rx) = mpsc:: channel ( 1 ) ;
1212+ registry. register ( "sbx" . to_string ( ) , "s1" . to_string ( ) , tx, make_shutdown ( ) ) ;
1213+
1214+ assert ! ( registry. finalize_main_process_exit( "sbx" ) ) ;
1215+ assert ! ( registry. terminal_delivery_finalized( "sbx" ) ) ;
1216+ assert_eq ! ( registry. remove_if_current( "sbx" , "s1" ) , Some ( true ) ) ;
11751217 }
11761218
11771219 // ---- open_relay: happy path and wait semantics ----
0 commit comments