@@ -58,6 +58,7 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
5858 /// </summary>
5959 private const int MinProtocolVersion = 3 ;
6060 private static readonly TimeSpan s_stderrPumpShutdownTimeout = TimeSpan . FromSeconds ( 5 ) ;
61+ private static readonly TimeSpan s_runtimeShutdownTimeout = TimeSpan . FromSeconds ( 10 ) ;
6162
6263 /// <summary>
6364 /// Provides a thread-safe collection of active Copilot sessions, indexed by session identifier.
@@ -291,7 +292,7 @@ async Task<Connection> StartCoreAsync(CancellationToken ct)
291292
292293 if ( connection is not null )
293294 {
294- await CleanupConnectionAsync ( connection , errors : null ) ;
295+ await CleanupConnectionAsync ( connection , errors : null , gracefulRuntimeShutdown : false ) ;
295296 }
296297 else if ( cliProcess is not null )
297298 {
@@ -312,6 +313,7 @@ async Task<Connection> StartCoreAsync(CancellationToken ct)
312313 /// This method performs graceful cleanup:
313314 /// <list type="number">
314315 /// <item>Closes all active sessions (releases in-memory resources)</item>
316+ /// <item>Requests runtime shutdown for SDK-owned CLI processes</item>
315317 /// <item>Closes the JSON-RPC connection</item>
316318 /// <item>Terminates the CLI server process (if spawned by this client)</item>
317319 /// </list>
@@ -346,7 +348,7 @@ public async Task StopAsync()
346348
347349 _sessions . Clear ( ) ;
348350
349- await CleanupConnectionAsync ( errors ) ;
351+ await CleanupConnectionAsync ( errors , gracefulRuntimeShutdown : true ) ;
350352
351353 ThrowErrors ( errors ) ;
352354 }
@@ -378,7 +380,7 @@ public async Task ForceStopAsync()
378380 _sessions . Clear ( ) ;
379381
380382 var errors = new List < Exception > ( ) ;
381- await CleanupConnectionAsync ( errors ) ;
383+ await CleanupConnectionAsync ( errors , gracefulRuntimeShutdown : false ) ;
382384 ThrowErrors ( errors ) ;
383385 }
384386
@@ -398,7 +400,7 @@ private static void ThrowErrors(List<Exception>? errors)
398400 }
399401 }
400402
401- private async Task CleanupConnectionAsync ( List < Exception > ? errors )
403+ private async Task CleanupConnectionAsync ( List < Exception > ? errors , bool gracefulRuntimeShutdown )
402404 {
403405 var connectionTask = _connectionTask ;
404406 if ( connectionTask is null )
@@ -419,11 +421,32 @@ private async Task CleanupConnectionAsync(List<Exception>? errors)
419421 return ;
420422 }
421423
422- await CleanupConnectionAsync ( ctx , errors ) ;
424+ await CleanupConnectionAsync ( ctx , errors , gracefulRuntimeShutdown ) ;
423425 }
424426
425- private async Task CleanupConnectionAsync ( Connection ctx , List < Exception > ? errors )
427+ private async Task CleanupConnectionAsync ( Connection ctx , List < Exception > ? errors , bool gracefulRuntimeShutdown )
426428 {
429+ var runtimeShutdownCompleted = false ;
430+ if ( gracefulRuntimeShutdown && ctx . CliProcess is not null )
431+ {
432+ var runtimeShutdownTimestamp = Stopwatch . GetTimestamp ( ) ;
433+ try
434+ {
435+ using var cancellation = new CancellationTokenSource ( s_runtimeShutdownTimeout ) ;
436+ await ctx . Server . Runtime . ShutdownAsync ( cancellation . Token ) ;
437+ runtimeShutdownCompleted = true ;
438+ LoggingHelpers . LogTiming ( _logger , LogLevel . Debug , null ,
439+ "CopilotClient.StopAsync runtime shutdown complete. Elapsed={Elapsed}" ,
440+ runtimeShutdownTimestamp ) ;
441+ }
442+ catch ( Exception ex )
443+ {
444+ LoggingHelpers . LogTiming ( _logger , LogLevel . Debug , ex ,
445+ "CopilotClient.StopAsync runtime shutdown failed. Elapsed={Elapsed}" ,
446+ runtimeShutdownTimestamp ) ;
447+ }
448+ }
449+
427450 try { ctx . Rpc . Dispose ( ) ; }
428451 catch ( Exception ex ) { AddCleanupError ( errors , ex , _logger ) ; }
429452
@@ -439,22 +462,62 @@ private async Task CleanupConnectionAsync(Connection ctx, List<Exception>? error
439462
440463 if ( ctx . CliProcess is { } childProcess )
441464 {
442- await CleanupCliProcessAsync ( childProcess , ctx . StderrPump , errors , _logger ) ;
465+ await CleanupCliProcessAsync ( childProcess , ctx . StderrPump , errors , _logger , runtimeShutdownCompleted ) ;
443466 }
444467 }
445468
446- private static async Task CleanupCliProcessAsync ( Process childProcess , ProcessStderrPump ? stderrPump , List < Exception > ? errors , ILogger ? logger )
469+ private static async Task CleanupCliProcessAsync ( Process childProcess , ProcessStderrPump ? stderrPump , List < Exception > ? errors , ILogger ? logger , bool waitForGracefulExit = false )
447470 {
448471 stderrPump ? . Cancel ( ) ;
449472
450473 try
451474 {
452475 if ( ! childProcess . HasExited )
453476 {
477+ if ( waitForGracefulExit )
478+ {
479+ var shutdownWaitTimestamp = Stopwatch . GetTimestamp ( ) ;
480+ try
481+ {
482+ await childProcess . WaitForExitAsync ( ) . WaitAsync ( s_runtimeShutdownTimeout ) ;
483+ }
484+ catch ( TimeoutException ex )
485+ {
486+ if ( logger is not null )
487+ {
488+ LoggingHelpers . LogTiming ( logger , LogLevel . Debug , ex ,
489+ "Timed out waiting for runtime process to exit after graceful shutdown. Elapsed={Elapsed}, Timeout={Timeout}" ,
490+ shutdownWaitTimestamp ,
491+ s_runtimeShutdownTimeout ) ;
492+ }
493+ }
494+ }
495+
496+ if ( childProcess . HasExited )
497+ {
498+ return ;
499+ }
500+
454501 childProcess . Kill ( entireProcessTree : true ) ;
455502 // Kill is asynchronous; wait for the root CLI process to exit so cleanup callers
456503 // do not observe StopAsync/DisposeAsync completion while it is still tearing down.
457- await childProcess . WaitForExitAsync ( ) ;
504+ var killWaitTimestamp = Stopwatch . GetTimestamp ( ) ;
505+ try
506+ {
507+ await childProcess . WaitForExitAsync ( ) . WaitAsync ( s_runtimeShutdownTimeout ) ;
508+ }
509+ catch ( TimeoutException ex )
510+ {
511+ if ( logger is not null )
512+ {
513+ LoggingHelpers . LogTiming ( logger , LogLevel . Debug , ex ,
514+ "Timed out waiting for runtime process to exit after kill. Elapsed={Elapsed}, Timeout={Timeout}" ,
515+ killWaitTimestamp ,
516+ s_runtimeShutdownTimeout ) ;
517+ }
518+
519+ AddCleanupError ( errors , ex , logger ) ;
520+ }
458521 }
459522 }
460523 catch ( Exception ex )
@@ -2002,9 +2065,10 @@ private async Task<Connection> ConnectToServerAsync(Process? cliProcess, string?
20022065 "CopilotClient.ConnectToServerAsync transport setup complete. Elapsed={Elapsed}" ,
20032066 setupTimestamp ) ;
20042067
2005- _serverRpc = new ServerRpc ( rpc ) ;
2068+ var connection = new Connection ( rpc , cliProcess , networkStream , stderrPump ) ;
2069+ _serverRpc = connection . Server ;
20062070
2007- return new Connection ( rpc , cliProcess , networkStream , stderrPump ) ;
2071+ return connection ;
20082072 }
20092073 catch
20102074 {
@@ -2208,6 +2272,7 @@ private class Connection(
22082272 {
22092273 public Process ? CliProcess => cliProcess ;
22102274 public JsonRpc Rpc => rpc ;
2275+ public ServerRpc Server => field ?? Interlocked . CompareExchange ( ref field , new ( rpc ) , null ) ?? field ;
22112276 public NetworkStream ? NetworkStream => networkStream ;
22122277 public ProcessStderrPump ? StderrPump => stderrPump ;
22132278 public StringBuilder ? StderrBuffer => stderrPump ? . Buffer ;
0 commit comments