Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
352 changes: 145 additions & 207 deletions .github/workflows/issue-triage.lock.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .github/workflows/issue-triage.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
description: "Issue number to triage"
required: true
type: string
if: secrets.COPILOT_GITHUB_TOKEN != ''
roles: all
permissions:
contents: read
Expand Down
366 changes: 145 additions & 221 deletions .github/workflows/sdk-consistency-review.lock.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .github/workflows/sdk-consistency-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ on:
description: "PR number to review"
required: true
type: string
if: secrets.COPILOT_GITHUB_TOKEN != ''
roles: all
permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
## 2025-05-15 - [Sequential session destruction in SDKs]
**Learning:** All Copilot SDKs (Node.js, Python, Go, .NET) were initially implementing session destruction sequentially during client shutdown. This leads to a linear increase in shutdown time as the number of active sessions grows, especially when individual destructions involve retries and backoff.
**Action:** Parallelize session cleanup using language-specific concurrency primitives (e.g., `Promise.all` in Node.js, `asyncio.gather` in Python, `Task.WhenAll` in .NET, or WaitGroups/Channels in Go) to ensure shutdown time remains constant and minimal.
**Action:** Parallelize session cleanup using language-specific concurrency primitives (e.g., `Promise.all` in Node.js, `asyncio.gather` in Python, `Task.WhenAll` in .NET, or WaitGroups/Channels in Go) to ensure shutdown time remains constant and minimal. This has now been implemented across all four SDKs.
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The statement "This has now been implemented across all four SDKs" is inaccurate. The Python SDK still implements sequential session destruction in its stop() method (lines 316-322 in python/copilot/client.py use a simple for loop with await). Only Node.js, Go, and .NET have parallelized session destruction after this PR. Consider updating this statement to reflect that Python still uses sequential destruction, or create a separate PR to parallelize Python's implementation as well.

Copilot uses AI. Check for mistakes.
13 changes: 10 additions & 3 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.Extensions.Logging.Abstractions;
using StreamJsonRpc;
using System.Collections.Concurrent;
using System.Linq;
using System.Data;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
Expand Down Expand Up @@ -212,17 +213,23 @@ public async Task StopAsync()
{
var errors = new List<Exception>();

foreach (var session in _sessions.Values.ToArray())
// Destroy sessions in parallel to avoid linear shutdown time
var destroyTasks = _sessions.Values.ToArray().Select(async session =>
{
try
{
await session.DisposeAsync();
}
catch (Exception ex)
{
errors.Add(new Exception($"Failed to destroy session {session.SessionId}: {ex.Message}", ex));
lock (errors)
{
errors.Add(new Exception($"Failed to destroy session {session.SessionId}: {ex.Message}", ex));
}
}
}
}).ToList();

await Task.WhenAll(destroyTasks);

_sessions.Clear();
await CleanupConnectionAsync(errors);
Expand Down
16 changes: 13 additions & 3 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,21 @@ func (c *Client) Stop() error {
}
c.sessionsMux.Unlock()

var wg sync.WaitGroup
var errsMux sync.Mutex
for _, session := range sessions {
if err := session.Destroy(); err != nil {
errs = append(errs, fmt.Errorf("failed to destroy session %s: %w", session.SessionID, err))
}
wg.Add(1)
go func(s *Session) {
defer wg.Done()
// Destroy sessions in parallel to avoid linear shutdown time
if err := s.Destroy(); err != nil {
errsMux.Lock()
errs = append(errs, fmt.Errorf("failed to destroy session %s: %w", s.SessionID, err))
errsMux.Unlock()
}
}(session)
}
wg.Wait()

c.sessionsMux.Lock()
c.sessions = make(map[string]*Session)
Expand Down
20 changes: 13 additions & 7 deletions python/copilot/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,13 +313,19 @@ async def stop(self) -> list["StopError"]:
sessions_to_destroy = list(self._sessions.values())
self._sessions.clear()

for session in sessions_to_destroy:
try:
await session.destroy()
except Exception as e:
errors.append(
StopError(message=f"Failed to destroy session {session.session_id}: {e}")
)
# Destroy sessions in parallel to avoid linear shutdown time
if sessions_to_destroy:
results = await asyncio.gather(
*[session.destroy() for session in sessions_to_destroy],
return_exceptions=True,
)
for session, result in zip(sessions_to_destroy, results):
if isinstance(result, Exception):
errors.append(
StopError(
message=f"Failed to destroy session {session.session_id}: {result}"
)
)

# Close client
if self._client:
Expand Down
Loading