@@ -435,6 +435,26 @@ public async Task SendAndWaitAsync_Completes_When_SessionIdle_Notification_Is_Dr
435435 Assert . Contains ( server . Requests , request => request . Method == "session.metadata.activity" ) ;
436436 }
437437
438+ [ Fact ]
439+ public async Task SendAndWaitAsync_Preserves_Event_Completion_When_Legacy_Runtime_Lacks_Fallback_Rpcs ( )
440+ {
441+ await using var server = await FakeCopilotServer . StartAsync ( ) ;
442+ server . ConfigureLegacyRuntimeCompletion ( ) ;
443+ await using var client = new CopilotClient ( new CopilotClientOptions { Connection = RuntimeConnection . ForUri ( server . Url ) } ) ;
444+ await using var session = await client . CreateSessionAsync ( new SessionConfig
445+ {
446+ OnPermissionRequest = PermissionHandler . ApproveAll
447+ } ) ;
448+
449+ var response = await session . SendAndWaitAsync (
450+ new MessageOptions { Prompt = "complete from delayed legacy idle" } ,
451+ timeout : TimeSpan . FromSeconds ( 3 ) ) ;
452+
453+ Assert . NotNull ( response ) ;
454+ Assert . Equal ( "completed response" , response . Data . Content ) ;
455+ Assert . Contains ( server . Requests , request => request . Method == "session.tasks.waitForPending" ) ;
456+ }
457+
438458 [ Fact ]
439459 public async Task SendAndWaitAsync_DroppedIdle_Fallback_Flushes_Preceding_Event_Handlers ( )
440460 {
@@ -624,6 +644,8 @@ private sealed class FakeCopilotServer : IAsyncDisposable
624644 private bool _failRuntimeShutdown ;
625645 private bool _emitCompletionWithoutIdle ;
626646 private bool _delayCompletionEvents ;
647+ private bool _emitDelayedIdle ;
648+ private bool _fallbackMethodsUnavailable ;
627649 private bool _reactivateDuringFinalBarrier ;
628650 private bool _hasActiveWork ;
629651 private int _activityRequestCount ;
@@ -702,6 +724,13 @@ public void ConfigureDroppedIdleCompletion(bool delayEvents = false)
702724 }
703725 }
704726
727+ public void ConfigureLegacyRuntimeCompletion ( )
728+ {
729+ ConfigureDroppedIdleCompletion ( ) ;
730+ _emitDelayedIdle = true ;
731+ _fallbackMethodsUnavailable = true ;
732+ }
733+
705734 public void ReleaseCompletionEvents ( )
706735 {
707736 _allowCompletionEvents . TrySetResult ( ) ;
@@ -786,6 +815,22 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel
786815 _requests . Add ( new RpcRequestRecord ( method ! , paramsElement ) ) ;
787816 }
788817
818+ if ( _fallbackMethodsUnavailable
819+ && method is "session.tasks.waitForPending" or "session.metadata.activity" )
820+ {
821+ await WriteMessageAsync ( stream , new Dictionary < string , object ? >
822+ {
823+ [ "jsonrpc" ] = "2.0" ,
824+ [ "id" ] = id ,
825+ [ "error" ] = new Dictionary < string , object ? >
826+ {
827+ [ "code" ] = - 32601 ,
828+ [ "message" ] = $ "Method not found: { method } "
829+ }
830+ } , cancellationToken ) ;
831+ return ;
832+ }
833+
789834 var activityRequestNumber = 0 ;
790835 if ( method == "session.metadata.activity" )
791836 {
@@ -926,6 +971,27 @@ private async Task EmitCompletionWithoutIdleAsync(Stream stream, CancellationTok
926971 }
927972 }
928973 } , cancellationToken ) ;
974+
975+ if ( _emitDelayedIdle )
976+ {
977+ await Task . Delay ( TimeSpan . FromMilliseconds ( 500 ) , cancellationToken ) ;
978+ await WriteMessageAsync ( stream , new Dictionary < string , object ? >
979+ {
980+ [ "jsonrpc" ] = "2.0" ,
981+ [ "method" ] = "session.event" ,
982+ [ "params" ] = new Dictionary < string , object ? >
983+ {
984+ [ "sessionId" ] = _lastSessionId ,
985+ [ "event" ] = new Dictionary < string , object ? >
986+ {
987+ [ "type" ] = "session.idle" ,
988+ [ "id" ] = Guid . NewGuid ( ) . ToString ( ) ,
989+ [ "timestamp" ] = DateTimeOffset . UtcNow . ToString ( "O" ) ,
990+ [ "data" ] = new Dictionary < string , object ? > ( )
991+ }
992+ }
993+ } , cancellationToken ) ;
994+ }
929995 }
930996
931997 private Dictionary < string , object ? > CreateSessionResult ( JsonElement request )
0 commit comments