Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
6 changes: 3 additions & 3 deletions .github/agents/docs-maintenance.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ cat nodejs/src/types.ts | grep -A 10 "export interface ExportSessionOptions"
**Must match:**
- `CopilotClient` constructor options: `cliPath`, `cliUrl`, `useStdio`, `port`, `logLevel`, `autoStart`, `autoRestart`, `env`, `githubToken`, `useLoggedInUser`
- `createSession()` config: `model`, `tools`, `hooks`, `systemMessage`, `mcpServers`, `availableTools`, `excludedTools`, `streaming`, `reasoningEffort`, `provider`, `infiniteSessions`, `customAgents`, `workingDirectory`
- `CopilotSession` methods: `send()`, `sendAndWait()`, `getMessages()`, `destroy()`, `abort()`, `on()`, `once()`, `off()`
- `CopilotSession` methods: `send()`, `sendAndWait()`, `getMessages()`, `disconnect()`, `abort()`, `on()`, `once()`, `off()`
- Hook names: `onPreToolUse`, `onPostToolUse`, `onUserPromptSubmitted`, `onSessionStart`, `onSessionEnd`, `onErrorOccurred`

#### Python Validation
Expand All @@ -362,7 +362,7 @@ cat python/copilot/types.py | grep -A 15 "class SessionHooks"
**Must match (snake_case):**
- `CopilotClient` options: `cli_path`, `cli_url`, `use_stdio`, `port`, `log_level`, `auto_start`, `auto_restart`, `env`, `github_token`, `use_logged_in_user`
- `create_session()` config keys: `model`, `tools`, `hooks`, `system_message`, `mcp_servers`, `available_tools`, `excluded_tools`, `streaming`, `reasoning_effort`, `provider`, `infinite_sessions`, `custom_agents`, `working_directory`
- `CopilotSession` methods: `send()`, `send_and_wait()`, `get_messages()`, `destroy()`, `abort()`, `export_session()`
- `CopilotSession` methods: `send()`, `send_and_wait()`, `get_messages()`, `disconnect()`, `abort()`, `export_session()`
- Hook names: `on_pre_tool_use`, `on_post_tool_use`, `on_user_prompt_submitted`, `on_session_start`, `on_session_end`, `on_error_occurred`

#### Go Validation
Expand All @@ -380,7 +380,7 @@ cat go/types.go | grep -A 15 "type SessionHooks struct"
**Must match (PascalCase for exported):**
- `ClientOptions` fields: `CLIPath`, `CLIUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `AutoRestart`, `Env`, `GithubToken`, `UseLoggedInUser`
- `SessionConfig` fields: `Model`, `Tools`, `Hooks`, `SystemMessage`, `MCPServers`, `AvailableTools`, `ExcludedTools`, `Streaming`, `ReasoningEffort`, `Provider`, `InfiniteSessions`, `CustomAgents`, `WorkingDirectory`
- `Session` methods: `Send()`, `SendAndWait()`, `GetMessages()`, `Destroy()`, `Abort()`, `ExportSession()`
- `Session` methods: `Send()`, `SendAndWait()`, `GetMessages()`, `Disconnect()`, `Abort()`, `ExportSession()`
- Hook fields: `OnPreToolUse`, `OnPostToolUse`, `OnUserPromptSubmitted`, `OnSessionStart`, `OnSessionEnd`, `OnErrorOccurred`

#### .NET Validation
Expand Down
2 changes: 1 addition & 1 deletion docs/auth/byok.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ async def main():
await session.send({"prompt": "What is 2+2?"})
await done.wait()

await session.destroy()
await session.disconnect()
await client.stop()

asyncio.run(main())
Expand Down
3 changes: 2 additions & 1 deletion docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b
| **Session Management** | | |
| Create session | `createSession()` | Full config support |
| Resume session | `resumeSession()` | With infinite session workspaces |
| Destroy session | `destroy()` | Clean up resources |
| Disconnect session | `disconnect()` | Release in-memory resources |
| Destroy session *(deprecated)* | `destroy()` | Use `disconnect()` instead |
| Delete session | `deleteSession()` | Remove from storage |
| List sessions | `listSessions()` | All stored sessions |
| Get last session | `getLastSessionId()` | For quick resume |
Expand Down
4 changes: 2 additions & 2 deletions docs/debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,9 @@ var client = new CopilotClient(new CopilotClientOptions

**Solution:**

1. Ensure you're not calling methods after `destroy()`:
1. Ensure you're not calling methods after `disconnect()`:
```typescript
await session.destroy();
await session.disconnect();
// Don't use session after this!
```

Expand Down
36 changes: 29 additions & 7 deletions docs/guides/session-persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,24 +325,46 @@ async function cleanupExpiredSessions(maxAgeMs: number) {
await cleanupExpiredSessions(24 * 60 * 60 * 1000);
```

### Explicit Session Destruction
### Disconnecting from a Session (`disconnect`)

When a task completes, destroy the session explicitly rather than waiting for timeouts:
When a task completes, disconnect from the session explicitly rather than waiting for timeouts. This releases in-memory resources but **preserves session data on disk**, so the session can still be resumed later:

```typescript
try {
// Do work...
await session.sendAndWait({ prompt: "Complete the task" });

// Task complete - clean up
await session.destroy();
// Task complete — release in-memory resources (session can be resumed later)
await session.disconnect();
} catch (error) {
// Clean up even on error
await session.destroy();
await session.disconnect();
throw error;
}
```

Each SDK also provides idiomatic automatic cleanup patterns:

| Language | Pattern | Example |
|----------|---------|---------|
| **TypeScript** | `Symbol.asyncDispose` | `await using session = await client.createSession(config);` |
| **Python** | `async with` context manager | `async with await client.create_session(config) as session:` |
| **C#** | `IAsyncDisposable` | `await using var session = await client.CreateSessionAsync(config);` |
| **Go** | `defer` | `defer session.Disconnect()` |

> **Note:** `destroy()` is deprecated in favor of `disconnect()`. Existing code using `destroy()` will continue to work but should be migrated.

### Permanently Deleting a Session (`deleteSession`)

To permanently remove a session and all its data from disk (conversation history, planning state, artifacts), use `deleteSession`. This is irreversible — the session **cannot** be resumed after deletion:

```typescript
// Permanently remove session data
await client.deleteSession("user-123-task-456");
```

> **`disconnect()` vs `deleteSession()`:** `disconnect()` releases in-memory resources but keeps session data on disk for later resumption. `deleteSession()` permanently removes everything, including files on disk.

## Automatic Cleanup: Idle Timeout

The CLI has a built-in 30-minute idle timeout. Sessions without activity are automatically cleaned up:
Expand Down Expand Up @@ -526,8 +548,8 @@ await withSessionLock("user-123-task-456", async () => {
| **Resume session** | `client.resumeSession(sessionId)` |
| **BYOK resume** | Re-provide `provider` config |
| **List sessions** | `client.listSessions(filter?)` |
| **Delete session** | `client.deleteSession(sessionId)` |
| **Destroy active session** | `session.destroy()` |
| **Disconnect from active session** | `session.disconnect()` — releases in-memory resources; session data on disk is preserved for resumption |
| **Delete session permanently** | `client.deleteSession(sessionId)` — permanently removes all session data from disk; cannot be resumed |
| **Containerized deployment** | Mount `~/.copilot/session-state/` to persistent storage |

## Next Steps
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/setup/azure-managed-identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class ManagedIdentityCopilotAgent:
session = await self.client.create_session(config)

response = await session.send_and_wait({"prompt": prompt})
await session.destroy()
await session.disconnect()

return response.data.content if response else ""
```
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/setup/backend-services.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ async function processJob(job: Job) {
});

await saveResult(job.id, response?.data.content);
await session.destroy(); // Clean up after job completes
await session.disconnect(); // Clean up after job completes
}
```

Expand Down
6 changes: 3 additions & 3 deletions docs/guides/setup/scaling.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,8 @@ class SessionManager {
private async evictOldestSession(): Promise<void> {
const [oldestId] = this.activeSessions.keys();
const session = this.activeSessions.get(oldestId)!;
// Session state is persisted automatically — safe to destroy
await session.destroy();
// Session state is persisted automatically — safe to disconnect
await session.disconnect();
this.activeSessions.delete(oldestId);
}
}
Expand Down Expand Up @@ -457,7 +457,7 @@ app.post("/api/analyze", async (req, res) => {
});
res.json({ result: response?.data.content });
} finally {
await session.destroy(); // Clean up immediately
await session.disconnect(); // Clean up immediately
}
});
```
Expand Down
4 changes: 2 additions & 2 deletions docs/mcp/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
defer session.Destroy()
defer session.Disconnect()

// Use the session...
}
Expand Down Expand Up @@ -191,7 +191,7 @@ async function main() {

console.log("Response:", result?.data?.content);

await session.destroy();
await session.disconnect();
await client.stop();
}

Expand Down
16 changes: 15 additions & 1 deletion dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,23 @@ Abort the currently processing message in this session.

Get all events/messages from this session.

##### `DisconnectAsync(CancellationToken cancellationToken = default): Task`

Disconnect the session and release in-memory resources. Session data on disk is preserved — the conversation can be resumed later via `ResumeSessionAsync()`. To permanently delete session data, use `client.DeleteSessionAsync()`.

##### `DisposeAsync(): ValueTask`

Dispose the session and free resources.
Calls `DisconnectAsync()`. Enables the `await using` pattern for automatic cleanup:

```csharp
// Preferred: automatic cleanup via await using
await using var session = await client.CreateSessionAsync(config);
// session is automatically disconnected when leaving scope

// Alternative: explicit disconnect
var session2 = await client.CreateSessionAsync(config);
await session2.DisconnectAsync();
Copy link
Contributor

Choose a reason for hiding this comment

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

For .NET we could make the case that we don't need both DisconnectAsync and DisposeAsync since they are exactly equivalent and DisposeAsync is sufficient on its own.

I don't feel strongly about this so if you prefer to keep DisconnectAsync that's fine, but if you don't have opinions I'd lean towards just having DisposeAsync as the more idiomatic standard.

Copy link
Collaborator

Choose a reason for hiding this comment

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

In #682 I propose adding a ShutdownAsync as a mechanism for developers that want to be able to receive shutdown events to get them; you can just use DisposeAsync, but if you want to reliably get shutdown events, you use ShutdownAsync, then wait for the shutdown events, and then DisposeAsync.

I suggest we shouldn't have three "I'm done" APIs. If we like the "disconnect" terminology, could "disconnect" be used instead of what I'm calling "shutdown"?

```

---

Expand Down
21 changes: 14 additions & 7 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,18 +211,23 @@ async Task<Connection> StartCoreAsync(CancellationToken ct)
}

/// <summary>
/// Disconnects from the Copilot server and stops all active sessions.
/// Disconnects from the Copilot server and closes all active sessions.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
/// <remarks>
/// <para>
/// This method performs graceful cleanup:
/// <list type="number">
/// <item>Destroys all active sessions</item>
/// <item>Closes all active sessions (releases in-memory resources)</item>
/// <item>Closes the JSON-RPC connection</item>
/// <item>Terminates the CLI server process (if spawned by this client)</item>
/// </list>
/// </para>
/// <para>
/// Note: session data on disk is preserved, so sessions can be resumed later.
/// To permanently remove session data before stopping, call
/// <see cref="DeleteSessionAsync"/> for each session first.
/// </para>
/// </remarks>
/// <exception cref="AggregateException">Thrown when multiple errors occur during cleanup.</exception>
/// <example>
Expand All @@ -238,11 +243,11 @@ public async Task StopAsync()
{
try
{
await session.DisposeAsync();
await session.DisconnectAsync();
}
catch (Exception ex)
{
errors.Add(new IOException($"Failed to destroy session {session.SessionId}: {ex.Message}", ex));
errors.Add(new Exception($"Failed to disconnect session {session.SessionId}: {ex.Message}", ex));
}
}

Expand Down Expand Up @@ -656,15 +661,17 @@ public async Task<List<ModelInfo>> ListModelsAsync(CancellationToken cancellatio
}

/// <summary>
/// Deletes a Copilot session by its ID.
/// Permanently deletes a session and all its data from disk, including
/// conversation history, planning state, and artifacts.
/// </summary>
/// <param name="sessionId">The ID of the session to delete.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
/// <returns>A task that represents the asynchronous delete operation.</returns>
/// <exception cref="InvalidOperationException">Thrown when the session does not exist or deletion fails.</exception>
/// <remarks>
/// This permanently removes the session and all its conversation history.
/// The session cannot be resumed after deletion.
/// Unlike <see cref="CopilotSession.DisconnectAsync"/>, which only releases in-memory
/// resources and preserves session data for later resumption, this method is
/// irreversible. The session cannot be resumed after deletion.
/// </remarks>
/// <example>
/// <code>
Expand Down
65 changes: 51 additions & 14 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ namespace GitHub.Copilot.SDK;
/// The session provides methods to send messages, subscribe to events, retrieve
/// conversation history, and manage the session lifecycle.
/// </para>
/// <para>
/// <see cref="CopilotSession"/> implements <see cref="IAsyncDisposable"/>. Use the
/// <c>await using</c> pattern for automatic cleanup, or call <see cref="DisconnectAsync"/>
/// explicitly. Disposing a session releases in-memory resources but preserves session data
/// on disk — the conversation can be resumed later via
/// <see cref="CopilotClient.ResumeSessionAsync"/>. To permanently delete session data,
/// use <see cref="CopilotClient.DeleteSessionAsync"/>.
/// </para>
/// </remarks>
/// <example>
/// <code>
Expand Down Expand Up @@ -522,31 +530,33 @@ public async Task SetModelAsync(string model, CancellationToken cancellationToke
}

/// <summary>
/// Disposes the <see cref="CopilotSession"/> and releases all associated resources.
/// Disconnects this session and releases all in-memory resources (event handlers,
/// tool handlers, permission handlers).
/// </summary>
/// <returns>A task representing the dispose operation.</returns>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the operation.</param>
/// <returns>A task representing the disconnect operation.</returns>
/// <remarks>
/// <para>
/// After calling this method, the session can no longer be used. All event handlers
/// and tool handlers are cleared.
/// Session state on disk (conversation history, planning state, artifacts) is
/// preserved, so the conversation can be resumed later by calling
/// <see cref="CopilotClient.ResumeSessionAsync"/> with the session ID. To
/// permanently remove all session data including files on disk, use
/// <see cref="CopilotClient.DeleteSessionAsync"/> instead.
/// </para>
/// <para>
/// To continue the conversation, use <see cref="CopilotClient.ResumeSessionAsync"/>
/// with the session ID.
/// After calling this method, the session object can no longer be used.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// // Using 'await using' for automatic disposal
/// await using var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll });
/// // Disconnect when done — session can still be resumed later
/// await session.DisconnectAsync();
///
/// // Or manually dispose
/// var session2 = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll });
/// // ... use the session ...
/// await session2.DisposeAsync();
/// // Or use 'await using' for automatic disconnection
/// await using var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll });
/// </code>
/// </example>
public async ValueTask DisposeAsync()
public async Task DisconnectAsync(CancellationToken cancellationToken = default)
{
if (Interlocked.Exchange(ref _isDisposed, 1) == 1)
{
Expand All @@ -556,7 +566,7 @@ public async ValueTask DisposeAsync()
try
{
await InvokeRpcAsync<object>(
"session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }], CancellationToken.None);
"session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }], cancellationToken);
}
catch (ObjectDisposedException)
{
Expand All @@ -573,6 +583,33 @@ await InvokeRpcAsync<object>(
_permissionHandler = null;
}

/// <summary>
/// Disposes the <see cref="CopilotSession"/> by disconnecting and releasing all resources.
/// </summary>
/// <returns>A task representing the dispose operation.</returns>
/// <remarks>
/// <para>
/// This method calls <see cref="DisconnectAsync"/> to perform cleanup. It is the
/// implementation of <see cref="IAsyncDisposable"/> and enables the
/// <c>await using</c> pattern for automatic resource management.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// // Using 'await using' for automatic disposal — session can still be resumed later
/// await using var session = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll });
///
/// // Or manually dispose
/// var session2 = await client.CreateSessionAsync(new() { OnPermissionRequest = PermissionHandler.ApproveAll });
/// // ... use the session ...
/// await session2.DisposeAsync();
/// </code>
/// </example>
public async ValueTask DisposeAsync()
{
await DisconnectAsync();
}

internal record SendMessageRequest
{
public string SessionId { get; init; } = string.Empty;
Expand Down
2 changes: 1 addition & 1 deletion dotnet/test/SessionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace GitHub.Copilot.SDK.Test;
public class SessionTests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "session", output)
{
[Fact]
public async Task ShouldCreateAndDestroySessions()
public async Task ShouldCreateAndDisconnectSessions()
{
var session = await CreateSessionAsync(new SessionConfig { Model = "fake-test-model" });

Expand Down
Loading
Loading