Skip to content

Commit 9d0b501

Browse files
committed
fix(dotnet): preserve legacy idle fallback
1 parent ab8ec0a commit 9d0b501

4 files changed

Lines changed: 76 additions & 94 deletions

File tree

dotnet/src/Session.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,16 @@ async Task MonitorRuntimeCompletionAsync(CancellationToken waitCancellationToken
443443
// existing event-only behavior and let session.idle or the timeout win.
444444
LogRuntimeCompletionFallbackUnavailable(ex, SessionId);
445445
}
446+
catch (IOException ex) when (ex.InnerException is RemoteRpcException
447+
{
448+
ErrorCode: RemoteRpcException.MethodNotFoundErrorCode
449+
})
450+
{
451+
// Generated RPC methods surface remote errors through CopilotClient,
452+
// which wraps them in IOException. Treat an older runtime's missing
453+
// fallback methods the same as a direct method-not-found response.
454+
LogRuntimeCompletionFallbackUnavailable(ex, SessionId);
455+
}
446456
catch (OperationCanceledException) when (waitCancellationToken.IsCancellationRequested)
447457
{
448458
// The timeout/caller-cancellation registration completes tcs.

dotnet/test/E2E/SendAndWaitReliabilityE2ETests.cs

Lines changed: 0 additions & 84 deletions
This file was deleted.

dotnet/test/Unit/ClientSessionLifetimeTests.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)

test/snapshots/send_and_wait_reliability/should_complete_when_live_sessionidle_is_dropped.yaml

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)