@@ -43,6 +43,8 @@ const HOOK_EVENT_NOTIFY_TIMEOUT: Duration = Duration::from_millis(750);
4343const DAEMON_SHUTDOWN_DEADLINE : Duration = Duration :: from_secs ( 45 ) ;
4444#[ cfg( unix) ]
4545const DAEMON_CLIENT_DRAIN_DEADLINE : Duration = Duration :: from_secs ( 15 ) ;
46+ #[ cfg( unix) ]
47+ const DAEMON_TASK_ABORT_DEADLINE : Duration = Duration :: from_secs ( 2 ) ;
4648
4749#[ derive( Clone , Default ) ]
4850pub ( crate ) struct DaemonLifecycle {
@@ -1534,22 +1536,40 @@ async fn run_foreground_unix(socket_path: PathBuf) -> Result<()> {
15341536 client_tasks. spawn ( async move { Box :: pin ( serve_socket_client ( stream, engine) ) . await } ) ;
15351537 }
15361538 engine. lifecycle . begin_draining ( ) ;
1537- log_daemon_event (
1538- "daemon_shutdown" ,
1539- & [ ( "socket" , socket_path. display ( ) . to_string ( ) ) ] ,
1540- ) ;
15411539 // Stop accepting and unlink the socket before draining so clients that
15421540 // connect during shutdown get NotFound/ConnectionRefused (which they retry
15431541 // via `connect_with_restart_grace`) instead of a queued connection that
15441542 // will never be served.
15451543 drop ( listener) ;
15461544 let _ = std:: fs:: remove_file ( & socket_path) ;
1547- let clients_drained = drain_client_tasks ( & mut client_tasks, DAEMON_CLIENT_DRAIN_DEADLINE ) . await ;
1548- engine. lifecycle . wait_for_idle ( ) . await ;
1545+ // Keep auxiliary process creation blocked until every scheduler and client
1546+ // task is drained or abandoned. A killed app-server call may retry before
1547+ // unwinding, so a shorter guard leaves a shutdown-time respawn race.
1548+ let _codex_shutdown = crate :: sessions:: codex_app_server:: begin_codex_app_server_shutdown ( ) ;
1549+ // Stop automation before announcing shutdown or waiting for clients.
1550+ // Scheduler tasks may be inside a synchronous auxiliary-agent call, so
1551+ // shutdown also terminates their tracked process trees before joining.
1552+ engine. shutdown_automation_schedulers ( ) . await ;
1553+ log_daemon_event (
1554+ "daemon_shutdown" ,
1555+ & [ ( "socket" , socket_path. display ( ) . to_string ( ) ) ] ,
1556+ ) ;
1557+ let in_flight_drained = timeout (
1558+ DAEMON_CLIENT_DRAIN_DEADLINE ,
1559+ engine. lifecycle . wait_for_idle ( ) ,
1560+ )
1561+ . await
1562+ . is_ok ( ) ;
1563+ // Once admitted requests are finished (or their bound elapsed), every
1564+ // remaining client task is an idle socket reader or already-cancelled
1565+ // request wrapper. Abort those immediately instead of making shutdown wait
1566+ // for clients to close persistent connections themselves.
1567+ client_tasks. abort_all ( ) ;
1568+ let clients_drained = drain_client_tasks ( & mut client_tasks, DAEMON_TASK_ABORT_DEADLINE ) . await ;
15491569 // Client setup and in-flight requests may create schedulers or project
15501570 // servers. Sweep owned background tasks only after all client work drains.
15511571 engine. shutdown_background_tasks ( ) . await ;
1552- if !clients_drained {
1572+ if !in_flight_drained || ! clients_drained {
15531573 log_daemon_event (
15541574 "daemon_shutdown" ,
15551575 & [
@@ -1618,9 +1638,12 @@ async fn drain_client_tasks(clients: &mut JoinSet<Result<()>>, deadline: Duratio
16181638 }
16191639
16201640 clients. abort_all ( ) ;
1621- while let Some ( completed) = clients. join_next ( ) . await {
1622- log_client_task_result ( completed) ;
1623- }
1641+ let _ = timeout ( DAEMON_TASK_ABORT_DEADLINE , async {
1642+ while let Some ( completed) = clients. join_next ( ) . await {
1643+ log_client_task_result ( completed) ;
1644+ }
1645+ } )
1646+ . await ;
16241647 false
16251648}
16261649
@@ -1905,6 +1928,9 @@ impl DaemonEngine {
19051928 project_path : PathBuf ,
19061929 handshake : DaemonHandshake ,
19071930 ) {
1931+ if !self . lifecycle . accepting ( ) {
1932+ return ;
1933+ }
19081934 {
19091935 let schedulers = self . automation_schedulers . lock ( ) . await ;
19101936 if schedulers. contains_key ( & key) {
@@ -1976,8 +2002,11 @@ impl DaemonEngine {
19762002 project_path : PathBuf ,
19772003 handshake : DaemonHandshake ,
19782004 ) {
2005+ if !self . lifecycle . accepting ( ) {
2006+ return ;
2007+ }
19792008 let mut schedulers = self . automation_schedulers . lock ( ) . await ;
1980- if schedulers. contains_key ( & key) {
2009+ if ! self . lifecycle . accepting ( ) || schedulers. contains_key ( & key) {
19812010 return ;
19822011 }
19832012 let wake = Arc :: new ( tokio:: sync:: Notify :: new ( ) ) ;
@@ -1994,14 +2023,7 @@ impl DaemonEngine {
19942023 }
19952024
19962025 async fn shutdown_background_tasks ( & self ) {
1997- let scheduler_handles: Vec < JoinHandle < ( ) > > = {
1998- let mut schedulers = self . automation_schedulers . lock ( ) . await ;
1999- schedulers. drain ( ) . map ( |( _, handle) | handle. task ) . collect ( )
2000- } ;
2001- for handle in scheduler_handles {
2002- handle. abort ( ) ;
2003- let _ = handle. await ;
2004- }
2026+ self . shutdown_automation_schedulers ( ) . await ;
20052027
20062028 self . git_watcher . shutdown ( ) . await ;
20072029 if let Some ( handle) = self . pr_autotrack_task . lock ( ) . await . take ( ) {
@@ -2010,6 +2032,23 @@ impl DaemonEngine {
20102032 }
20112033 }
20122034
2035+ async fn shutdown_automation_schedulers ( & self ) {
2036+ let scheduler_handles: Vec < JoinHandle < ( ) > > = {
2037+ let mut schedulers = self . automation_schedulers . lock ( ) . await ;
2038+ schedulers. drain ( ) . map ( |( _, handle) | handle. task ) . collect ( )
2039+ } ;
2040+ let _child_shutdown = crate :: sessions:: codex_app_server:: begin_codex_app_server_shutdown ( ) ;
2041+ for handle in & scheduler_handles {
2042+ handle. abort ( ) ;
2043+ }
2044+ let _ = timeout ( DAEMON_TASK_ABORT_DEADLINE , async {
2045+ for handle in scheduler_handles {
2046+ let _ = handle. await ;
2047+ }
2048+ } )
2049+ . await ;
2050+ }
2051+
20132052 async fn shutdown_servers ( & self ) {
20142053 let servers: Vec < Arc < crate :: mcp:: McpServer > > = {
20152054 let servers = self . project_servers . lock ( ) . await ;
0 commit comments