Repository navigation
SessionStartEvent and SessionShutdownEvent never fire via CopilotSession.On for .NET #535
Description
I ran into these two issues while trying to create my own OpenTelemetry instrumentor for copilot sessions on .NET
SessionStartEvent never fires in .On() registration
The event fires during CreateSessionAsync(), which prevents ever seeing it as part of a .On() registration. The event IS emitted via GetMessagesAsync(). This makes sense technically (the session was created before the registration happened), however it makes the API difficult to consume. It forces consumers to capture existing events and replay them for logging, and to do so in the right order and without double emits).
SessionShutdownEvent appears to never fire
I can't seem to get this one to fire at all, either from On() or from GetMessagesAsync(), even when forcing shutdown of the session while keeping the On subscription alive.
Repo
Here's a single file repro you can use to see what I'm seeing. Run with dotnet Program.cs with .NET 10:
#!/usr/bin/env dotnet
#:package GitHub.Copilot.SDK@0.1.26-preview.0
// Bug repro: SessionStartEvent and SessionShutdownEvent never fire
// when subscribing to CopilotSession events via session.On(...).
//
// This repro addresses potential timing/scoping objections:
// 1. Subscription is created immediately after CreateSessionAsync
// 2. Subscription is kept alive through session disposal (manual dispose order)
// 3. GetMessagesAsync checks the event history independently
//
// SDK version: GitHub.Copilot.SDK 0.1.26-preview.0
using GitHub.Copilot.SDK;
using var client = new CopilotClient();
await client.StartAsync(CancellationToken.None);
string workDir = Path.Combine(Path.GetTempPath(), $"sdk-model-repro-{Guid.NewGuid():N}");
Directory.CreateDirectory(workDir);
try
{
var config = new SessionConfig
{
Model = "claude-sonnet-4",
WorkingDirectory = workDir,
};
Console.WriteLine($"SessionConfig.Model = \"{config.Model}\"");
Console.WriteLine();
var receivedEvents = new List<string>();
// Create session, then subscribe immediately (no gap for missed events)
var session = await client.CreateSessionAsync(config);
var sub = session.On(evt =>
{
receivedEvents.Add(evt.GetType().Name);
switch (evt)
{
case SessionStartEvent start:
Console.WriteLine($"[SessionStartEvent]");
Console.WriteLine($" SelectedModel = {Quote(start.Data?.SelectedModel)}");
break;
case AssistantTurnStartEvent:
Console.WriteLine($"[AssistantTurnStartEvent]");
break;
case AssistantUsageEvent usage:
Console.WriteLine($"[AssistantUsageEvent]");
Console.WriteLine($" Model = {Quote(usage.Data?.Model)}");
Console.WriteLine($" InputTokens = {usage.Data?.InputTokens}");
Console.WriteLine($" OutputTokens = {usage.Data?.OutputTokens}");
break;
case AssistantTurnEndEvent:
Console.WriteLine($"[AssistantTurnEndEvent]");
break;
case SessionShutdownEvent shutdown:
Console.WriteLine($"[SessionShutdownEvent]");
Console.WriteLine($" CurrentModel = {Quote(shutdown.Data?.CurrentModel)}");
Console.WriteLine($" ShutdownType = {shutdown.Data?.ShutdownType}");
break;
}
});
Console.WriteLine("Sending prompt...");
Console.WriteLine();
try
{
await session.SendAndWaitAsync(
new MessageOptions { Prompt = "Reply with exactly: hello" },
timeout: TimeSpan.FromMinutes(2));
}
catch (Exception ex)
{
Console.WriteLine($"SendAndWaitAsync threw: {ex.Message}");
}
// Check event history independently of the subscription
Console.WriteLine();
Console.WriteLine("--- Event history (GetMessagesAsync) ---");
var history = await session.GetMessagesAsync(CancellationToken.None);
foreach (var evt in history)
Console.WriteLine($" {evt.GetType().Name}");
// Dispose session while subscription is still active
Console.WriteLine();
Console.WriteLine("--- Disposing session (subscription still active) ---");
await session.DisposeAsync();
await Task.Delay(1000); // allow any async event delivery
// Now dispose subscription
sub.Dispose();
// Diagnosis
Console.WriteLine();
Console.WriteLine("--- Diagnosis ---");
var historyTypes = history.Select(e => e.GetType().Name).ToHashSet();
Console.WriteLine($"SessionStartEvent in history: {historyTypes.Contains("SessionStartEvent"),-5} delivered to On(): {receivedEvents.Contains("SessionStartEvent")}");
Console.WriteLine($"SessionShutdownEvent in history: {historyTypes.Contains("SessionShutdownEvent"),-5} delivered to On(): {receivedEvents.Contains("SessionShutdownEvent")}");
}
finally
{
try { Directory.Delete(workDir, recursive: true); } catch { }
}
static string Quote(string? value) => value is null ? "<null>" : $"\"{value}\"";
Activity
github-actions commentedon Feb 22, 2026
Thanks for opening this issue and providing such a detailed reproduction!
I've labeled this as
bugandsdk/dotnetsince it's specific to the .NET SDK event handling behavior.The issue is clear:
SessionStartEventfires duringCreateSessionAsync()but never reaches.On()subscribers (though it appears inGetMessagesAsync())SessionShutdownEventdoesn't fire at all, even with explicit disposalYour reproduction code is excellent and will help the team investigate. This appears to be a legitimate timing/event delivery issue in the .NET SDK's event subscription mechanism.
theblixguy commentedon Feb 23, 2026
Related #490
SteveSandersonMS commentedon Mar 10, 2026
Thanks - fixed in #664
feat: add OnDisconnected event to CopilotClient