1111using System . Diagnostics ;
1212using System . Diagnostics . CodeAnalysis ;
1313using System . Net . Sockets ;
14+ using System . Text ;
1415using System . Text . Json ;
1516using System . Text . Json . Serialization ;
1617using System . Text . RegularExpressions ;
@@ -183,13 +184,13 @@ async Task<Connection> StartCoreAsync(CancellationToken ct)
183184 if ( _optionsHost is not null && _optionsPort is not null )
184185 {
185186 // External server (TCP)
186- result = ConnectToServerAsync ( null , _optionsHost , _optionsPort , ct ) ;
187+ result = ConnectToServerAsync ( null , _optionsHost , _optionsPort , null , ct ) ;
187188 }
188189 else
189190 {
190191 // Child process (stdio or TCP)
191- var ( cliProcess , portOrNull ) = await StartCliServerAsync ( _options , _logger , ct ) ;
192- result = ConnectToServerAsync ( cliProcess , portOrNull is null ? null : "localhost" , portOrNull , ct ) ;
192+ var ( cliProcess , portOrNull , stderrBuffer ) = await StartCliServerAsync ( _options , _logger , ct ) ;
193+ result = ConnectToServerAsync ( cliProcess , portOrNull is null ? null : "localhost" , portOrNull , stderrBuffer , ct ) ;
193194 }
194195
195196 var connection = await result ;
@@ -842,11 +843,33 @@ private void DispatchLifecycleEvent(SessionLifecycleEvent evt)
842843 }
843844
844845 internal static async Task < T > InvokeRpcAsync < T > ( JsonRpc rpc , string method , object ? [ ] ? args , CancellationToken cancellationToken )
846+ {
847+ return await InvokeRpcAsync < T > ( rpc , method , args , null , cancellationToken ) ;
848+ }
849+
850+ internal static async Task < T > InvokeRpcAsync < T > ( JsonRpc rpc , string method , object ? [ ] ? args , StringBuilder ? stderrBuffer , CancellationToken cancellationToken )
845851 {
846852 try
847853 {
848854 return await rpc . InvokeWithCancellationAsync < T > ( method , args , cancellationToken ) ;
849855 }
856+ catch ( StreamJsonRpc . ConnectionLostException ex )
857+ {
858+ string ? stderrOutput = null ;
859+ if ( stderrBuffer is not null )
860+ {
861+ lock ( stderrBuffer )
862+ {
863+ stderrOutput = stderrBuffer . ToString ( ) . Trim ( ) ;
864+ }
865+ }
866+
867+ if ( ! string . IsNullOrEmpty ( stderrOutput ) )
868+ {
869+ throw new IOException ( $ "CLI process exited unexpectedly.\n stderr: { stderrOutput } ", ex ) ;
870+ }
871+ throw new IOException ( $ "Communication error with Copilot CLI: { ex . Message } ", ex ) ;
872+ }
850873 catch ( StreamJsonRpc . RemoteRpcException ex )
851874 {
852875 throw new IOException ( $ "Communication error with Copilot CLI: { ex . Message } ", ex ) ;
@@ -868,7 +891,7 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
868891 {
869892 var expectedVersion = SdkProtocolVersion . GetVersion ( ) ;
870893 var pingResponse = await InvokeRpcAsync < PingResponse > (
871- connection . Rpc , "ping" , [ new PingRequest ( ) ] , cancellationToken ) ;
894+ connection . Rpc , "ping" , [ new PingRequest ( ) ] , connection . StderrBuffer , cancellationToken ) ;
872895
873896 if ( ! pingResponse . ProtocolVersion . HasValue )
874897 {
@@ -887,7 +910,7 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
887910 }
888911 }
889912
890- private static async Task < ( Process Process , int ? DetectedLocalhostTcpPort ) > StartCliServerAsync ( CopilotClientOptions options , ILogger logger , CancellationToken cancellationToken )
913+ private static async Task < ( Process Process , int ? DetectedLocalhostTcpPort , StringBuilder StderrBuffer ) > StartCliServerAsync ( CopilotClientOptions options , ILogger logger , CancellationToken cancellationToken )
891914 {
892915 // Use explicit path or bundled CLI - no PATH fallback
893916 var cliPath = options . CliPath ?? GetBundledCliPath ( out var searchedPath )
@@ -957,14 +980,19 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
957980 var cliProcess = new Process { StartInfo = startInfo } ;
958981 cliProcess . Start ( ) ;
959982
960- // Forward stderr to logger
983+ // Capture stderr for error messages and forward to logger
984+ var stderrBuffer = new StringBuilder ( ) ;
961985 _ = Task . Run ( async ( ) =>
962986 {
963987 while ( cliProcess != null && ! cliProcess . HasExited )
964988 {
965989 var line = await cliProcess . StandardError . ReadLineAsync ( cancellationToken ) ;
966990 if ( line != null )
967991 {
992+ lock ( stderrBuffer )
993+ {
994+ stderrBuffer . AppendLine ( line ) ;
995+ }
968996 logger . LogDebug ( "[CLI] {Line}" , line ) ;
969997 }
970998 }
@@ -991,7 +1019,7 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
9911019 }
9921020 }
9931021
994- return ( cliProcess , detectedLocalhostTcpPort ) ;
1022+ return ( cliProcess , detectedLocalhostTcpPort , stderrBuffer ) ;
9951023 }
9961024
9971025 private static string ? GetBundledCliPath ( out string searchedPath )
@@ -1035,7 +1063,7 @@ private static (string FileName, IEnumerable<string> Args) ResolveCliCommand(str
10351063 return ( cliPath , args ) ;
10361064 }
10371065
1038- private async Task < Connection > ConnectToServerAsync ( Process ? cliProcess , string ? tcpHost , int ? tcpPort , CancellationToken cancellationToken )
1066+ private async Task < Connection > ConnectToServerAsync ( Process ? cliProcess , string ? tcpHost , int ? tcpPort , StringBuilder ? stderrBuffer , CancellationToken cancellationToken )
10391067 {
10401068 Stream inputStream , outputStream ;
10411069 TcpClient ? tcpClient = null ;
@@ -1080,7 +1108,7 @@ private async Task<Connection> ConnectToServerAsync(Process? cliProcess, string?
10801108
10811109 _rpc = new ServerRpc ( rpc ) ;
10821110
1083- return new Connection ( rpc , cliProcess , tcpClient , networkStream ) ;
1111+ return new Connection ( rpc , cliProcess , tcpClient , networkStream , stderrBuffer ) ;
10841112 }
10851113
10861114 [ UnconditionalSuppressMessage ( "Trimming" , "IL2026" , Justification = "Using happy path from https://microsoft.github.io/vs-streamjsonrpc/docs/nativeAOT.html" ) ]
@@ -1321,12 +1349,14 @@ private class Connection(
13211349 JsonRpc rpc ,
13221350 Process ? cliProcess , // Set if we created the child process
13231351 TcpClient ? tcpClient , // Set if using TCP
1324- NetworkStream ? networkStream ) // Set if using TCP
1352+ NetworkStream ? networkStream , // Set if using TCP
1353+ StringBuilder ? stderrBuffer = null ) // Captures stderr for error messages
13251354 {
13261355 public Process ? CliProcess => cliProcess ;
13271356 public TcpClient ? TcpClient => tcpClient ;
13281357 public JsonRpc Rpc => rpc ;
13291358 public NetworkStream ? NetworkStream => networkStream ;
1359+ public StringBuilder ? StderrBuffer => stderrBuffer ;
13301360 }
13311361
13321362 private static class ProcessArgumentEscaper
0 commit comments