@@ -235,15 +235,15 @@ async Task<Connection> StartCoreAsync(CancellationToken ct)
235235 {
236236 // External server (TCP)
237237 _actualPort = _optionsPort ;
238- connection = await ConnectToServerAsync ( null , _optionsHost , _optionsPort , null , ct ) ;
238+ connection = await ConnectToServerAsync ( null , _optionsHost , _optionsPort , null , null , ct ) ;
239239 }
240240 else
241241 {
242242 // Child process (stdio or TCP)
243- var ( startedProcess , portOrNull , stderrBuffer ) = await StartCliServerAsync ( _options , _effectiveConnectionToken , _logger , ct ) ;
243+ var ( startedProcess , portOrNull , stderrBuffer , stderrReader ) = await StartCliServerAsync ( _options , _effectiveConnectionToken , _logger , ct ) ;
244244 cliProcess = startedProcess ;
245245 _actualPort = portOrNull ;
246- connection = await ConnectToServerAsync ( cliProcess , portOrNull is null ? null : "localhost" , portOrNull , stderrBuffer , ct ) ;
246+ connection = await ConnectToServerAsync ( cliProcess , portOrNull is null ? null : "localhost" , portOrNull , stderrBuffer , stderrReader , ct ) ;
247247 }
248248
249249 // Verify protocol version compatibility
@@ -1134,21 +1134,19 @@ internal static async Task InvokeRpcAsync(JsonRpc rpc, string method, object?[]?
11341134 }
11351135
11361136 internal static async Task < T > InvokeRpcAsync < T > ( JsonRpc rpc , string method , object ? [ ] ? args , StringBuilder ? stderrBuffer , CancellationToken cancellationToken )
1137+ {
1138+ return await InvokeRpcAsync < T > ( rpc , method , args , stderrBuffer , stderrReader : null , cancellationToken ) ;
1139+ }
1140+
1141+ internal static async Task < T > InvokeRpcAsync < T > ( JsonRpc rpc , string method , object ? [ ] ? args , StringBuilder ? stderrBuffer , Task ? stderrReader , CancellationToken cancellationToken )
11371142 {
11381143 try
11391144 {
11401145 return await rpc . InvokeAsync < T > ( method , args , cancellationToken ) ;
11411146 }
11421147 catch ( ConnectionLostException ex )
11431148 {
1144- string ? stderrOutput = null ;
1145- if ( stderrBuffer is not null )
1146- {
1147- lock ( stderrBuffer )
1148- {
1149- stderrOutput = stderrBuffer . ToString ( ) . Trim ( ) ;
1150- }
1151- }
1149+ var stderrOutput = await GetStderrOutputAsync ( stderrBuffer , stderrReader ) ;
11521150
11531151 if ( ! string . IsNullOrEmpty ( stderrOutput ) )
11541152 {
@@ -1169,13 +1167,34 @@ private static string FormatCliExitedMessage(string message, string stderrOutput
11691167 : $ "{ message } \n stderr: { stderrOutput } ";
11701168 }
11711169
1172- private static IOException CreateCliExitedException ( string message , StringBuilder stderrBuffer )
1170+ private static async Task < string ? > GetStderrOutputAsync ( StringBuilder ? stderrBuffer , Task ? stderrReader )
1171+ {
1172+ if ( stderrBuffer is null )
1173+ {
1174+ return null ;
1175+ }
1176+
1177+ var stderrOutput = ReadStderrBuffer ( stderrBuffer ) ;
1178+ if ( string . IsNullOrEmpty ( stderrOutput ) && stderrReader is not null )
1179+ {
1180+ await Task . WhenAny ( stderrReader , Task . Delay ( TimeSpan . FromMilliseconds ( 250 ) ) ) ;
1181+ stderrOutput = ReadStderrBuffer ( stderrBuffer ) ;
1182+ }
1183+
1184+ return string . IsNullOrEmpty ( stderrOutput ) ? null : stderrOutput ;
1185+ }
1186+
1187+ private static string ReadStderrBuffer ( StringBuilder stderrBuffer )
11731188 {
1174- string stderrOutput ;
11751189 lock ( stderrBuffer )
11761190 {
1177- stderrOutput = stderrBuffer . ToString ( ) . Trim ( ) ;
1191+ return stderrBuffer . ToString ( ) . Trim ( ) ;
11781192 }
1193+ }
1194+
1195+ private static IOException CreateCliExitedException ( string message , StringBuilder stderrBuffer )
1196+ {
1197+ var stderrOutput = ReadStderrBuffer ( stderrBuffer ) ;
11791198
11801199 return new IOException ( FormatCliExitedMessage ( message , stderrOutput ) ) ;
11811200 }
@@ -1229,15 +1248,15 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
12291248 try
12301249 {
12311250 var connectResponse = await InvokeRpcAsync < ConnectResult > (
1232- connection . Rpc , "connect" , [ new ConnectRequest { Token = _effectiveConnectionToken } ] , connection . StderrBuffer , cancellationToken ) ;
1251+ connection . Rpc , "connect" , [ new ConnectRequest { Token = _effectiveConnectionToken } ] , connection . StderrBuffer , connection . StderrReader , cancellationToken ) ;
12331252 serverVersion = ( int ) connectResponse . ProtocolVersion ;
12341253 }
12351254 catch ( IOException ex ) when ( ex . InnerException is RemoteRpcException remoteEx && IsUnsupportedConnectMethod ( remoteEx ) )
12361255 {
12371256 // Legacy server without `connect`; fall back to `ping`. A token, if any,
12381257 // is silently dropped — the legacy server can't enforce one.
12391258 var pingResponse = await InvokeRpcAsync < PingResponse > (
1240- connection . Rpc , "ping" , [ new PingRequest ( ) ] , connection . StderrBuffer , cancellationToken ) ;
1259+ connection . Rpc , "ping" , [ new PingRequest ( ) ] , connection . StderrBuffer , connection . StderrReader , cancellationToken ) ;
12411260 serverVersion = pingResponse . ProtocolVersion ;
12421261 }
12431262
@@ -1266,7 +1285,7 @@ private static bool IsUnsupportedConnectMethod(RemoteRpcException ex)
12661285 || string . Equals ( ex . Message , "Unhandled method connect" , StringComparison . Ordinal ) ;
12671286 }
12681287
1269- private static async Task < ( Process Process , int ? DetectedLocalhostTcpPort , StringBuilder StderrBuffer ) > StartCliServerAsync ( CopilotClientOptions options , string ? connectionToken , ILogger logger , CancellationToken cancellationToken )
1288+ private static async Task < ( Process Process , int ? DetectedLocalhostTcpPort , StringBuilder StderrBuffer , Task StderrReader ) > StartCliServerAsync ( CopilotClientOptions options , string ? connectionToken , ILogger logger , CancellationToken cancellationToken )
12701289 {
12711290 // Use explicit path, COPILOT_CLI_PATH env var (from options.Environment or process env), or bundled CLI - no PATH fallback
12721291 var envCliPath = options . Environment is not null && options . Environment . TryGetValue ( "COPILOT_CLI_PATH" , out var envValue ) ? envValue
@@ -1417,7 +1436,7 @@ private static bool IsUnsupportedConnectMethod(RemoteRpcException ex)
14171436 }
14181437 }
14191438
1420- return ( cliProcess , detectedLocalhostTcpPort , stderrBuffer ) ;
1439+ return ( cliProcess , detectedLocalhostTcpPort , stderrBuffer , stderrReader ) ;
14211440 }
14221441 catch
14231442 {
@@ -1471,7 +1490,7 @@ private static (string FileName, IEnumerable<string> Args) ResolveCliCommand(str
14711490 return ( cliPath , args ) ;
14721491 }
14731492
1474- private async Task < Connection > ConnectToServerAsync ( Process ? cliProcess , string ? tcpHost , int ? tcpPort , StringBuilder ? stderrBuffer , CancellationToken cancellationToken )
1493+ private async Task < Connection > ConnectToServerAsync ( Process ? cliProcess , string ? tcpHost , int ? tcpPort , StringBuilder ? stderrBuffer , Task ? stderrReader , CancellationToken cancellationToken )
14751494 {
14761495 Stream inputStream , outputStream ;
14771496 NetworkStream ? networkStream = null ;
@@ -1537,7 +1556,7 @@ private async Task<Connection> ConnectToServerAsync(Process? cliProcess, string?
15371556
15381557 _serverRpc = new ServerRpc ( rpc ) ;
15391558
1540- return new Connection ( rpc , cliProcess , networkStream , stderrBuffer ) ;
1559+ return new Connection ( rpc , cliProcess , networkStream , stderrBuffer , stderrReader ) ;
15411560 }
15421561
15431562 private static JsonSerializerOptions SerializerOptionsForMessageFormatter { get ; } = CreateSerializerOptions ( ) ;
@@ -1752,12 +1771,14 @@ private class Connection(
17521771 JsonRpc rpc ,
17531772 Process ? cliProcess , // Set if we created the child process
17541773 NetworkStream ? networkStream , // Set if using TCP
1755- StringBuilder ? stderrBuffer = null ) // Captures stderr for error messages
1774+ StringBuilder ? stderrBuffer = null , // Captures stderr for error messages
1775+ Task ? stderrReader = null )
17561776 {
17571777 public Process ? CliProcess => cliProcess ;
17581778 public JsonRpc Rpc => rpc ;
17591779 public NetworkStream ? NetworkStream => networkStream ;
17601780 public StringBuilder ? StderrBuffer => stderrBuffer ;
1781+ public Task ? StderrReader => stderrReader ;
17611782 }
17621783
17631784 private static class ProcessArgumentEscaper
0 commit comments