Skip to content

.NET: runtime logs forwarding session.idle, but SDK callback never delivers it #2008

Description

@nicolasdossantos

Summary

Visual Studio can remain indefinitely in the running state after the Copilot CLI has completed a response.

The CLI verbose log shows that it forwarded the terminal, ephemeral session.idle event. The .NET SDK subscription receives the preceding events, including the final assistant.turn_end, but never receives SessionIdleEvent.

Because session.idle is ephemeral, it cannot be recovered later through GetEventsAsync(). The client therefore has no authoritative completion signal and continues waiting.

Environment

  • GitHub.Copilot.SDK: 1.0.1
  • Copilot CLI runtime: 1.0.61
  • Windows ARM64
  • Visual Studio Agent and Agent (Preview) modes

The current SDK 1.0.7 still does not expose a durable idle wait or event-dispatch flush API.

Observed behavior

The CLI log ends with:

Forwarding event ... assistant.turn_end
Forwarding event ... session.idle (ephemeral)

The .NET subscriber observes assistant.turn_end, but not session.idle. The operation remains active even though the runtime has finished.

The absence of session.idle from events.jsonl is expected because the event is ephemeral; it does not explain why the live subscriber missed it.

Why assistant.turn_end is insufficient

In multi-turn tool workflows, an intermediate assistant.turn_end can be followed immediately by another assistant.turn_start. Completion also needs to account for sub-agents, attached shells, queued messages, and follow-up notification turns.

Requested fix

Please provide an authoritative completion contract owned by the runtime/SDK, for example:

await session.WaitForIdleAsync(cancellationToken);

It should:

  1. Wait for turns, sub-agents, attached shells, queued work, and follow-up turns to settle.
  2. Resolve only when the runtime is fully idle.
  3. Ensure preceding session events have crossed the .NET SDK FIFO dispatch queue.
  4. Surface session errors and transport failures.
  5. Remain correct if the live session.idle notification is dropped.
  6. Support cancellation without leaving an orphaned waiter.

An equivalent session.waitForIdle runtime RPC combined with an SDK dispatch barrier would also work.

Regression test

Add a .NET test where a multi-turn response completes, the live session.idle notification is deliberately dropped, and the completion wait still resolves only after all preceding assistant and tool events have been delivered.

Related

Activity

  1. github-actions commented on Jul 16, 2026

    @github-actions
    Contributor

    Investigation findings

    Label applied: bug

    I investigated the reported behavior by examining the following files:

    • dotnet/src/Session.cs — event channel and SendAndWaitAsync implementation
    • dotnet/src/Client.cs — OnSessionEvent / OnSessionLifecycle RPC handlers and method registrations
    • dotnet/src/JsonRpc.cs — JSON-RPC dispatch
    • dotnet/src/Generated/SessionEvents.cs — SessionIdleEvent deserialization

    What I found

    The SDK architecture is designed to deliver session.idle reliably to live subscribers:

    1. session.idle is properly registered as a [JsonDerivedType] and will be deserialized to SessionIdleEvent.
    2. OnSessionEvent in Client.cs calls session.DispatchEvent(evt), which TryWrites it into an unbounded Channel<SessionEvent> and then ProcessEventsAsync delivers it serially to all registered handlers — no filtering or intentional dropping occurs in the SDK.
    3. SendAndWaitAsync registers its SessionIdleEvent handler before calling SendAsync, so there is no subscription race window on the SDK side.
    4. The CLI confirms it forwards the ephemeral session.idle event — yet the SDK subscriber misses it in practice on Windows ARM64.

    Root cause assessment

    This is a genuine bug. The SDK's completion contract (SendAndWaitAsync) has no fallback when the live session.idle notification is dropped in transit. Because the event is ephemeral it cannot be recovered via GetEventsAsync(), leaving the caller waiting indefinitely (until the 60 s timeout fires).

    Previous issue #794 identified this drop and proposed a fallback that was never shipped. Issue #558 tracks the resulting timeout. The combination confirms this is a known but unresolved transport reliability gap, not intentional design.

    What a fix should address

    1. Resilient idle detection: the runtime/SDK should provide a WaitForIdleAsync-style RPC call that is answered by the runtime rather than relying solely on an ephemeral push notification.
    2. Dispatch flush guarantee: callers awaiting completion need assurance that all preceding events have been delivered to FIFO handlers before the idle signal resolves.
    3. Regression test: a test that deliberately drops the live session.idle notification and verifies the completion wait still resolves (e.g., via a polled RPC fallback) would prevent recurrence.

    I have applied the bug label accordingly.

    Warning

    Firewall blocked 1 domain

    The following domain was blocked by the firewall during workflow execution:

    • awmgmcpg

    To allow these domains, add them to the network.allowed list in your workflow frontmatter:

    network:
      allowed:
        - defaults
        - "awmgmcpg"

    See Network Configuration for more information.

    Generated by Bug Handler for #2008 · 52.8 AIC · ⌖ 5.09 AIC · ⊞ 4.5K · ◷

  2. SteveSandersonMS commented on Jul 28, 2026

    @SteveSandersonMS
    Contributor

    Thanks for reporting this, and especially for including the CLI-side event sequence. We want to ensure Visual Studio has a reliable completion contract.

    One requested requirement is:

    Remain correct if the live session.idle notification is dropped.

    The transport is intended to provide reliable, ordered event delivery. We generally cannot support individual events being unpredictably dropped: losing assistant, tool, permission, or error events could break much more than idle detection. We are not currently aware of a confirmed case where one event disappears while the connection remains healthy. If that can be reproduced, we would prioritize investigating and fixing the delivery defect itself.

    There is a separate consideration around resume: session.idle is ephemeral and is not persisted or replayed. A completed session resumed later will not receive another copy of its previous idle event. An active resumed session should receive a future idle event when its work finishes.

    Could you share a small code sample or pseudocode showing:

    • whether the session is created or resumed;
    • if resumed, whether Visual Studio expects the prior idle state or sends new work first;
    • whether it uses SendAndWaitAsync or an event subscription; and
    • the short event sequence observed by that subscription?

    For example:

    var session = await client.CreateSessionAsync(...); // or ResumeSessionAsync(...)
    session.On<SessionEvent>(evt => Log(evt.Type));
    
    await session.SendAsync(...); // or SendAndWaitAsync(...)

    If the same live subscription receives assistant.turn_end for newly sent work but not the following session.idle, that would be particularly important. A minimal console reproduction would be ideal, but stripped-down pseudocode from the Visual Studio flow is enough to begin; no wire-level trace is needed at this stage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions