Skip to content
Merged
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
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
12 changes: 11 additions & 1 deletion dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,17 @@ Get all events/messages from this session.

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

Dispose the session and free resources.
Close 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()`.

```csharp
// Preferred: automatic cleanup via await using
await using var session = await client.CreateSessionAsync(config);
// session is automatically disposed when leaving scope
// Alternative: explicit dispose
var session2 = await client.CreateSessionAsync(config);
await session2.DisposeAsync();
```

---

Expand Down
19 changes: 13 additions & 6 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 @@ -242,7 +247,7 @@ public async Task StopAsync()
}
catch (Exception ex)
{
errors.Add(new IOException($"Failed to destroy session {session.SessionId}: {ex.Message}", ex));
errors.Add(new Exception($"Failed to dispose 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.DisposeAsync"/>, 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
23 changes: 17 additions & 6 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="DisposeAsync"/>
/// 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,22 +530,25 @@ public async Task SetModelAsync(string model, CancellationToken cancellationToke
}

/// <summary>
/// Disposes the <see cref="CopilotSession"/> and releases all associated resources.
/// Closes this session and releases all in-memory resources (event handlers,
/// tool handlers, permission handlers).
/// </summary>
/// <returns>A task representing the dispose 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
/// // 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
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
7 changes: 4 additions & 3 deletions go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
defer session.Destroy()
defer session.Disconnect()

// Set up event handler
done := make(chan bool)
Expand Down Expand Up @@ -169,7 +169,8 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec
- `On(handler SessionEventHandler) func()` - Subscribe to events (returns unsubscribe function)
- `Abort(ctx context.Context) error` - Abort the currently processing message
- `GetMessages(ctx context.Context) ([]SessionEvent, error)` - Get message history
- `Destroy() error` - Destroy the session
- `Disconnect() error` - Disconnect the session (releases in-memory resources, preserves disk state)
- `Destroy() error` - *(Deprecated)* Use `Disconnect()` instead

### Helper Functions

Expand Down Expand Up @@ -310,7 +311,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
defer session.Destroy()
defer session.Disconnect()

done := make(chan bool)

Expand Down
17 changes: 12 additions & 5 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,10 +293,14 @@ func (c *Client) Start(ctx context.Context) error {
// Stop stops the CLI server and closes all active sessions.
//
// This method performs graceful cleanup:
// 1. Destroys all active sessions
// 1. Closes all active sessions (releases in-memory resources)
// 2. Closes the JSON-RPC connection
// 3. Terminates the CLI server process (if spawned by this client)
//
// Note: session data on disk is preserved, so sessions can be resumed later.
// To permanently remove session data before stopping, call [Client.DeleteSession]
// for each session first.
//
// Returns an error that aggregates all errors encountered during cleanup.
//
// Example:
Expand All @@ -307,7 +311,7 @@ func (c *Client) Start(ctx context.Context) error {
func (c *Client) Stop() error {
var errs []error

// Destroy all active sessions
// Disconnect all active sessions
c.sessionsMux.Lock()
sessions := make([]*Session, 0, len(c.sessions))
for _, session := range c.sessions {
Expand All @@ -316,8 +320,8 @@ func (c *Client) Stop() error {
c.sessionsMux.Unlock()

for _, session := range sessions {
if err := session.Destroy(); err != nil {
errs = append(errs, fmt.Errorf("failed to destroy session %s: %w", session.SessionID, err))
if err := session.Disconnect(); err != nil {
errs = append(errs, fmt.Errorf("failed to disconnect session %s: %w", session.SessionID, err))
}
}

Expand Down Expand Up @@ -685,8 +689,11 @@ func (c *Client) ListSessions(ctx context.Context, filter *SessionListFilter) ([
return response.Sessions, nil
}

// DeleteSession permanently deletes a session and all its conversation history.
// DeleteSession permanently deletes a session and all its data from disk,
// including conversation history, planning state, and artifacts.
//
// Unlike [Session.Disconnect], which only releases in-memory resources and
// preserves session data for later resumption, DeleteSession is irreversible.
// The session cannot be resumed after deletion. If the session is in the local
// sessions map, it will be removed.
//
Expand Down
Loading
Loading