From 4377e73f5db849e380a803d975632d7cb44ec85f Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Thu, 26 Feb 2026 21:15:40 -0800 Subject: [PATCH 1/5] docs: clarify session destroy vs delete semantics across all SDKs Clarify the distinction between destroy() (closes session, releases in-memory resources, preserves disk state for resumption) and deleteSession() (permanently removes all data from disk). Update doc comments across all four SDK languages (Go, Node.js, Python, .NET) and the session persistence guide to make the behavioral difference explicit and help users choose the right method. Fixes #526 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/guides/session-persistence.md | 21 ++++++++++++++++----- dotnet/src/Client.cs | 17 ++++++++++++----- dotnet/src/Session.cs | 15 +++++++++------ go/client.go | 11 +++++++++-- go/session.go | 14 +++++++++----- nodejs/src/client.ts | 14 ++++++++++---- nodejs/src/session.ts | 17 +++++++++++------ python/copilot/client.py | 14 ++++++++++---- python/copilot/session.py | 15 ++++++++++----- 9 files changed, 96 insertions(+), 42 deletions(-) diff --git a/docs/guides/session-persistence.md b/docs/guides/session-persistence.md index 527f5ecc..dec91d5f 100644 --- a/docs/guides/session-persistence.md +++ b/docs/guides/session-persistence.md @@ -325,16 +325,16 @@ async function cleanupExpiredSessions(maxAgeMs: number) { await cleanupExpiredSessions(24 * 60 * 60 * 1000); ``` -### Explicit Session Destruction +### Closing a Session (`destroy`) -When a task completes, destroy the session explicitly rather than waiting for timeouts: +When a task completes, close 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 + // Task complete — release in-memory resources (session can be resumed later) await session.destroy(); } catch (error) { // Clean up even on error @@ -343,6 +343,17 @@ try { } ``` +### 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"); +``` + +> **`destroy()` vs `deleteSession()`:** `destroy()` 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: @@ -526,8 +537,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()` | +| **Close active session** | `session.destroy()` — 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 diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 1f3a7fb4..40e15326 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -210,18 +210,23 @@ async Task StartCoreAsync(CancellationToken ct) } /// - /// Disconnects from the Copilot server and stops all active sessions. + /// Disconnects from the Copilot server and closes all active sessions. /// /// A representing the asynchronous operation. /// /// /// This method performs graceful cleanup: /// - /// Destroys all active sessions + /// Closes all active sessions (releases in-memory resources) /// Closes the JSON-RPC connection /// 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 + /// for each session first. + /// /// /// Thrown when multiple errors occur during cleanup. /// @@ -655,15 +660,17 @@ public async Task> ListModelsAsync(CancellationToken cancellatio } /// - /// Deletes a Copilot session by its ID. + /// Permanently deletes a session and all its data from disk, including + /// conversation history, planning state, and artifacts. /// /// The ID of the session to delete. /// A that can be used to cancel the operation. /// A task that represents the asynchronous delete operation. /// Thrown when the session does not exist or deletion fails. /// - /// This permanently removes the session and all its conversation history. - /// The session cannot be resumed after deletion. + /// Unlike , which only releases in-memory + /// resources and preserves session data for later resumption, this method is + /// irreversible. The session cannot be resumed after deletion. /// /// /// diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 923b193c..02bb276d 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -541,22 +541,25 @@ await InvokeRpcAsync( } /// - /// Disposes the and releases all associated resources. + /// Closes this session and releases all in-memory resources (event handlers, + /// tool handlers, permission handlers). /// /// A task representing the dispose operation. /// /// - /// 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 + /// with the session ID. To + /// permanently remove all session data including files on disk, use + /// instead. /// /// - /// To continue the conversation, use - /// with the session ID. + /// After calling this method, the session object can no longer be used. /// /// /// /// - /// // 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 diff --git a/go/client.go b/go/client.go index c88a68ac..f140127b 100644 --- a/go/client.go +++ b/go/client.go @@ -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: @@ -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.Destroy], 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. // diff --git a/go/session.go b/go/session.go index 12d1b1af..561e566d 100644 --- a/go/session.go +++ b/go/session.go @@ -511,17 +511,21 @@ func (s *Session) GetMessages(ctx context.Context) ([]SessionEvent, error) { return response.Events, nil } -// Destroy destroys this session and releases all associated resources. +// Destroy closes this session and releases all in-memory resources (event +// handlers, tool handlers, permission handlers). // -// After calling this method, the session can no longer be used. All event -// handlers and tool handlers are cleared. To continue the conversation, -// use [Client.ResumeSession] with the session ID. +// Session state on disk (conversation history, planning state, artifacts) is +// preserved, so the conversation can be resumed later by calling +// [Client.ResumeSession] with the session ID. To permanently remove all +// session data including files on disk, use [Client.DeleteSession] instead. +// +// After calling this method, the session object can no longer be used. // // Returns an error if the connection fails. // // Example: // -// // Clean up when done +// // Clean up when done — session can still be resumed later // if err := session.Destroy(); err != nil { // log.Printf("Failed to destroy session: %v", err) // } diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 6d841c7c..c8c81dc7 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -307,10 +307,14 @@ export class CopilotClient { * Stops the CLI server and closes all active sessions. * * This method performs graceful cleanup: - * 1. Destroys all active sessions with retry logic + * 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 + * {@link deleteSession} for each session first. + * * @returns A promise that resolves with an array of errors encountered during cleanup. * An empty array indicates all cleanup succeeded. * @@ -823,10 +827,12 @@ export class CopilotClient { } /** - * Deletes a session and its data from disk. + * Permanently deletes a session and all its data from disk, including + * conversation history, planning state, and artifacts. * - * This permanently removes the session and all its conversation history. - * The session cannot be resumed after deletion. + * Unlike {@link CopilotSession.destroy}, which only releases in-memory + * resources and preserves session data for later resumption, this method + * is irreversible. The session cannot be resumed after deletion. * * @param sessionId - The ID of the session to delete * @returns A promise that resolves when the session is deleted diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 04525d2b..6e32ee5f 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -499,18 +499,23 @@ export class CopilotSession { } /** - * Destroys this session and releases all associated resources. + * Closes this session and releases all in-memory resources (event handlers, + * tool handlers, permission handlers). * - * After calling this method, the session can no longer be used. All event - * handlers and tool handlers are cleared. To continue the conversation, - * use {@link CopilotClient.resumeSession} with the session ID. + * Session state on disk (conversation history, planning state, artifacts) is + * preserved, so the conversation can be resumed later by calling + * {@link CopilotClient.resumeSession} with the session ID. To permanently + * remove all session data including files on disk, use + * {@link CopilotClient.deleteSession} instead. * - * @returns A promise that resolves when the session is destroyed + * After calling this method, the session object can no longer be used. + * + * @returns A promise that resolves when the session is closed * @throws Error if the connection fails * * @example * ```typescript - * // Clean up when done + * // Clean up when done — session can still be resumed later * await session.destroy(); * ``` */ diff --git a/python/copilot/client.py b/python/copilot/client.py index c25e6809..2d50c73e 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -320,10 +320,14 @@ async def stop(self) -> list["StopError"]: Stop the CLI server and close 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 + :meth:`delete_session` for each session first. + Returns: A list of StopError objects containing error messages that occurred during cleanup. An empty list indicates all cleanup succeeded. @@ -928,10 +932,12 @@ async def list_sessions( async def delete_session(self, session_id: str) -> None: """ - Delete a session permanently. + Permanently delete a session and all its data from disk, including + conversation history, planning state, and artifacts. - This permanently removes the session and all its conversation history. - The session cannot be resumed after deletion. + Unlike :meth:`CopilotSession.destroy`, which only releases in-memory + resources and preserves session data for later resumption, this method + is irreversible. The session cannot be resumed after deletion. Args: session_id: The ID of the session to delete. diff --git a/python/copilot/session.py b/python/copilot/session.py index a02dcf1e..72c08530 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -476,17 +476,22 @@ async def get_messages(self) -> list[SessionEvent]: async def destroy(self) -> None: """ - Destroy this session and release all associated resources. + Close this session and release all in-memory resources (event handlers, + tool handlers, permission handlers). - After calling this method, the session can no longer be used. All event - handlers and tool handlers are cleared. To continue the conversation, - use :meth:`CopilotClient.resume_session` with the session ID. + Session state on disk (conversation history, planning state, artifacts) + is preserved, so the conversation can be resumed later by calling + :meth:`CopilotClient.resume_session` with the session ID. To + permanently remove all session data including files on disk, use + :meth:`CopilotClient.delete_session` instead. + + After calling this method, the session object can no longer be used. Raises: Exception: If the connection fails. Example: - >>> # Clean up when done + >>> # Clean up when done — session can still be resumed later >>> await session.destroy() """ await self._client.request("session.destroy", {"sessionId": self.session_id}) From 1e6f1ee98be8425eef852e16928390fca7e44af1 Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Wed, 4 Mar 2026 19:23:35 -0800 Subject: [PATCH 2/5] feat: add disconnect() method, deprecate destroy() across all SDKs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add disconnect() as the preferred method for closing sessions across all four SDK languages, marking destroy() as deprecated: - Node.js: disconnect() + Symbol.asyncDispose support, destroy() delegates - Python: disconnect() + __aenter__/__aexit__ context manager, destroy() emits DeprecationWarning - Go: Disconnect() method, Destroy() marked with Deprecated godoc tag - .NET: DisconnectAsync() method, DisposeAsync() delegates to it Update all samples, READMEs, and documentation guides to use the new disconnect() terminology. Internal stop() methods now call disconnect(). Resolves PR #599 comments: - Rename destroy → disconnect for clarity - Define IDisposable behavior in .NET (DisposeAsync delegates to DisconnectAsync) - Add idiomatic cleanup patterns (async context managers, Symbol.asyncDispose) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/auth/byok.md | 2 +- docs/debugging.md | 4 +- docs/guides/session-persistence.md | 14 +++--- docs/guides/setup/azure-managed-identity.md | 2 +- docs/guides/setup/backend-services.md | 2 +- docs/guides/setup/scaling.md | 4 +- docs/mcp/overview.md | 4 +- dotnet/src/Client.cs | 6 +-- dotnet/src/Session.cs | 48 ++++++++++++++++----- go/README.md | 4 +- go/client.go | 8 ++-- go/samples/chat.go | 2 +- go/session.go | 26 +++++++---- nodejs/README.md | 2 +- nodejs/examples/basic-example.ts | 2 +- nodejs/src/client.ts | 10 ++--- nodejs/src/session.ts | 36 ++++++++++++---- python/README.md | 6 +-- python/copilot/client.py | 8 ++-- python/copilot/session.py | 42 +++++++++++++++--- 20 files changed, 157 insertions(+), 75 deletions(-) diff --git a/docs/auth/byok.md b/docs/auth/byok.md index 13ad8b05..ca7861c1 100644 --- a/docs/auth/byok.md +++ b/docs/auth/byok.md @@ -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()) diff --git a/docs/debugging.md b/docs/debugging.md index 6183cccd..bf953b2f 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -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! ``` diff --git a/docs/guides/session-persistence.md b/docs/guides/session-persistence.md index dec91d5f..c34ee5dd 100644 --- a/docs/guides/session-persistence.md +++ b/docs/guides/session-persistence.md @@ -325,9 +325,9 @@ async function cleanupExpiredSessions(maxAgeMs: number) { await cleanupExpiredSessions(24 * 60 * 60 * 1000); ``` -### Closing a Session (`destroy`) +### Disconnecting from a Session (`disconnect`) -When a task completes, close 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: +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 { @@ -335,14 +335,16 @@ try { await session.sendAndWait({ prompt: "Complete the task" }); // Task complete — release in-memory resources (session can be resumed later) - await session.destroy(); + await session.disconnect(); } catch (error) { // Clean up even on error - await session.destroy(); + await session.disconnect(); throw error; } ``` +> **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: @@ -352,7 +354,7 @@ To permanently remove a session and all its data from disk (conversation history await client.deleteSession("user-123-task-456"); ``` -> **`destroy()` vs `deleteSession()`:** `destroy()` releases in-memory resources but keeps session data on disk for later resumption. `deleteSession()` permanently removes everything, including files on disk. +> **`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 @@ -537,7 +539,7 @@ await withSessionLock("user-123-task-456", async () => { | **Resume session** | `client.resumeSession(sessionId)` | | **BYOK resume** | Re-provide `provider` config | | **List sessions** | `client.listSessions(filter?)` | -| **Close active session** | `session.destroy()` — releases in-memory resources; session data on disk is preserved for resumption | +| **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 | diff --git a/docs/guides/setup/azure-managed-identity.md b/docs/guides/setup/azure-managed-identity.md index bfafc6f9..9ad1ddb1 100644 --- a/docs/guides/setup/azure-managed-identity.md +++ b/docs/guides/setup/azure-managed-identity.md @@ -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 "" ``` diff --git a/docs/guides/setup/backend-services.md b/docs/guides/setup/backend-services.md index c9bc13f8..e0d0975d 100644 --- a/docs/guides/setup/backend-services.md +++ b/docs/guides/setup/backend-services.md @@ -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 } ``` diff --git a/docs/guides/setup/scaling.md b/docs/guides/setup/scaling.md index fcdb716d..5b74dee5 100644 --- a/docs/guides/setup/scaling.md +++ b/docs/guides/setup/scaling.md @@ -413,7 +413,7 @@ class SessionManager { const [oldestId] = this.activeSessions.keys(); const session = this.activeSessions.get(oldestId)!; // Session state is persisted automatically — safe to destroy - await session.destroy(); + await session.disconnect(); this.activeSessions.delete(oldestId); } } @@ -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 } }); ``` diff --git a/docs/mcp/overview.md b/docs/mcp/overview.md index aa2fba66..5ad8b1df 100644 --- a/docs/mcp/overview.md +++ b/docs/mcp/overview.md @@ -132,7 +132,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() // Use the session... } @@ -191,7 +191,7 @@ async function main() { console.log("Response:", result?.data?.content); - await session.destroy(); + await session.disconnect(); await client.stop(); } diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 40e15326..50eb9390 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -242,11 +242,11 @@ public async Task StopAsync() { try { - await session.DisposeAsync(); + await session.DisconnectAsync(); } catch (Exception ex) { - errors.Add(new Exception($"Failed to destroy session {session.SessionId}: {ex.Message}", ex)); + errors.Add(new Exception($"Failed to disconnect session {session.SessionId}: {ex.Message}", ex)); } } @@ -668,7 +668,7 @@ public async Task> ListModelsAsync(CancellationToken cancellatio /// A task that represents the asynchronous delete operation. /// Thrown when the session does not exist or deletion fails. /// - /// Unlike , which only releases in-memory + /// Unlike , which only releases in-memory /// resources and preserves session data for later resumption, this method is /// irreversible. The session cannot be resumed after deletion. /// diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 02bb276d..b03a0de7 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -541,10 +541,11 @@ await InvokeRpcAsync( } /// - /// Closes this session and releases all in-memory resources (event handlers, + /// Disconnects this session and releases all in-memory resources (event handlers, /// tool handlers, permission handlers). /// - /// A task representing the dispose operation. + /// A that can be used to cancel the operation. + /// A task representing the disconnect operation. /// /// /// Session state on disk (conversation history, planning state, artifacts) is @@ -559,16 +560,14 @@ await InvokeRpcAsync( /// /// /// - /// // Using 'await using' for automatic disposal — session can still be resumed later - /// 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 }); /// /// - public async ValueTask DisposeAsync() + public async Task DisconnectAsync(CancellationToken cancellationToken = default) { if (Interlocked.Exchange(ref _isDisposed, 1) == 1) { @@ -578,7 +577,7 @@ public async ValueTask DisposeAsync() try { await InvokeRpcAsync( - "session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }], CancellationToken.None); + "session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }], cancellationToken); } catch (ObjectDisposedException) { @@ -592,7 +591,7 @@ await InvokeRpcAsync( _eventHandlers.Clear(); _toolHandlers.Clear(); - await _permissionHandlerLock.WaitAsync(); + await _permissionHandlerLock.WaitAsync(cancellationToken); try { _permissionHandler = null; @@ -603,6 +602,33 @@ await InvokeRpcAsync( } } + /// + /// Disposes the by disconnecting and releasing all resources. + /// + /// A task representing the dispose operation. + /// + /// + /// This method calls to perform cleanup. It is the + /// implementation of and enables the + /// await using pattern for automatic resource management. + /// + /// + /// + /// + /// // 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(); + /// + /// + public async ValueTask DisposeAsync() + { + await DisconnectAsync(); + } + private class OnDisposeCall(Action callback) : IDisposable { public void Dispose() => callback(); diff --git a/go/README.md b/go/README.md index b010fc21..1ba496aa 100644 --- a/go/README.md +++ b/go/README.md @@ -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) @@ -297,7 +297,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() done := make(chan bool) diff --git a/go/client.go b/go/client.go index f140127b..144c90e4 100644 --- a/go/client.go +++ b/go/client.go @@ -311,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 { @@ -320,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)) } } @@ -692,7 +692,7 @@ func (c *Client) ListSessions(ctx context.Context, filter *SessionListFilter) ([ // DeleteSession permanently deletes a session and all its data from disk, // including conversation history, planning state, and artifacts. // -// Unlike [Session.Destroy], which only releases in-memory resources and +// 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. diff --git a/go/samples/chat.go b/go/samples/chat.go index 4fc11ffd..f984f758 100644 --- a/go/samples/chat.go +++ b/go/samples/chat.go @@ -30,7 +30,7 @@ func main() { if err != nil { panic(err) } - defer session.Destroy() + defer session.Disconnect() session.On(func(event copilot.SessionEvent) { var output string diff --git a/go/session.go b/go/session.go index 561e566d..d913c20c 100644 --- a/go/session.go +++ b/go/session.go @@ -34,7 +34,7 @@ type sessionHandler struct { // if err != nil { // log.Fatal(err) // } -// defer session.Destroy() +// defer session.Disconnect() // // // Subscribe to events // unsubscribe := session.On(func(event copilot.SessionEvent) { @@ -97,7 +97,7 @@ func newSession(sessionID string, client *jsonrpc2.Client, workspacePath string) // - options: The message options including the prompt and optional attachments. // // Returns the message ID of the response, which can be used to correlate events, -// or an error if the session has been destroyed or the connection fails. +// or an error if the session has been disconnected or the connection fails. // // Example: // @@ -483,7 +483,7 @@ func (s *Session) dispatchEvent(event SessionEvent) { // assistant responses, tool executions, and other session events in // chronological order. // -// Returns an error if the session has been destroyed or the connection fails. +// Returns an error if the session has been disconnected or the connection fails. // // Example: // @@ -511,7 +511,7 @@ func (s *Session) GetMessages(ctx context.Context) ([]SessionEvent, error) { return response.Events, nil } -// Destroy closes this session and releases all in-memory resources (event +// Disconnect closes this session and releases all in-memory resources (event // handlers, tool handlers, permission handlers). // // Session state on disk (conversation history, planning state, artifacts) is @@ -526,13 +526,13 @@ func (s *Session) GetMessages(ctx context.Context) ([]SessionEvent, error) { // Example: // // // Clean up when done — session can still be resumed later -// if err := session.Destroy(); err != nil { -// log.Printf("Failed to destroy session: %v", err) +// if err := session.Disconnect(); err != nil { +// log.Printf("Failed to disconnect session: %v", err) // } -func (s *Session) Destroy() error { +func (s *Session) Disconnect() error { _, err := s.client.Request("session.destroy", sessionDestroyRequest{SessionID: s.SessionID}) if err != nil { - return fmt.Errorf("failed to destroy session: %w", err) + return fmt.Errorf("failed to disconnect session: %w", err) } // Clear handlers @@ -551,12 +551,20 @@ func (s *Session) Destroy() error { return nil } +// Deprecated: Use [Session.Disconnect] instead. Destroy will be removed in a future release. +// +// Destroy closes this session and releases all in-memory resources. +// Session data on disk is preserved for later resumption. +func (s *Session) Destroy() error { + return s.Disconnect() +} + // Abort aborts the currently processing message in this session. // // Use this to cancel a long-running request. The session remains valid // and can continue to be used for new messages. // -// Returns an error if the session has been destroyed or the connection fails. +// Returns an error if the session has been disconnected or the connection fails. // // Example: // diff --git a/nodejs/README.md b/nodejs/README.md index 31558b8a..64f39674 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -52,7 +52,7 @@ await session.send({ prompt: "What is 2+2?" }); await done; // Clean up -await session.destroy(); +await session.disconnect(); await client.stop(); ``` diff --git a/nodejs/examples/basic-example.ts b/nodejs/examples/basic-example.ts index b0b99313..c20a85af 100644 --- a/nodejs/examples/basic-example.ts +++ b/nodejs/examples/basic-example.ts @@ -41,6 +41,6 @@ const result2 = await session.sendAndWait({ prompt: "Use lookup_fact to tell me console.log("📝 Response:", result2?.data.content); // Clean up -await session.destroy(); +await session.disconnect(); await client.stop(); console.log("✅ Done!"); diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index c8c81dc7..21d77251 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -102,7 +102,7 @@ function toJsonSchema(parameters: Tool["parameters"]): Record | * await session.send({ prompt: "Hello!" }); * * // Clean up - * await session.destroy(); + * await session.disconnect(); * await client.stop(); * ``` */ @@ -329,7 +329,7 @@ export class CopilotClient { async stop(): Promise { const errors: Error[] = []; - // Destroy all active sessions with retry logic + // Disconnect all active sessions with retry logic for (const session of this.sessions.values()) { const sessionId = session.sessionId; let lastError: Error | null = null; @@ -337,7 +337,7 @@ export class CopilotClient { // Try up to 3 times with exponential backoff for (let attempt = 1; attempt <= 3; attempt++) { try { - await session.destroy(); + await session.disconnect(); lastError = null; break; // Success } catch (error) { @@ -354,7 +354,7 @@ export class CopilotClient { if (lastError) { errors.push( new Error( - `Failed to destroy session ${sessionId} after 3 attempts: ${lastError.message}` + `Failed to disconnect session ${sessionId} after 3 attempts: ${lastError.message}` ) ); } @@ -830,7 +830,7 @@ export class CopilotClient { * Permanently deletes a session and all its data from disk, including * conversation history, planning state, and artifacts. * - * Unlike {@link CopilotSession.destroy}, which only releases in-memory + * Unlike {@link CopilotSession.disconnect}, which only releases in-memory * resources and preserves session data for later resumption, this method * is irreversible. The session cannot be resumed after deletion. * diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 6e32ee5f..49b575b0 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -52,7 +52,7 @@ export type AssistantMessageEvent = Extract { + async disconnect(): Promise { await this.connection.sendRequest("session.destroy", { sessionId: this.sessionId, }); @@ -529,6 +529,24 @@ export class CopilotSession { this.permissionHandler = undefined; } + /** + * @deprecated Use {@link disconnect} instead. This method will be removed in a future release. + * + * Disconnects this session and releases all in-memory resources. + * Session data on disk is preserved for later resumption. + * + * @returns A promise that resolves when the session is disconnected + * @throws Error if the connection fails + */ + async destroy(): Promise { + return this.disconnect(); + } + + /** Enables `await using session = ...` syntax for automatic cleanup. */ + async [Symbol.asyncDispose](): Promise { + return this.disconnect(); + } + /** * Aborts the currently processing message in this session. * @@ -536,7 +554,7 @@ export class CopilotSession { * and can continue to be used for new messages. * * @returns A promise that resolves when the abort request is acknowledged - * @throws Error if the session has been destroyed or the connection fails + * @throws Error if the session has been disconnected or the connection fails * * @example * ```typescript diff --git a/python/README.md b/python/README.md index 3a1c4c73..a62bfd8b 100644 --- a/python/README.md +++ b/python/README.md @@ -51,7 +51,7 @@ async def main(): await done.wait() # Clean up - await session.destroy() + await session.disconnect() await client.stop() asyncio.run(main()) @@ -90,7 +90,7 @@ await session.send({"prompt": "Hello!"}) # ... wait for events ... -await session.destroy() +await session.disconnect() await client.stop() ``` @@ -277,7 +277,7 @@ async def main(): await session.send({"prompt": "Tell me a short story"}) await done.wait() # Wait for streaming to complete - await session.destroy() + await session.disconnect() await client.stop() asyncio.run(main()) diff --git a/python/copilot/client.py b/python/copilot/client.py index 2d50c73e..32e05bc5 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -100,7 +100,7 @@ class CopilotClient: >>> await session.send({"prompt": "Hello!"}) >>> >>> # Clean up - >>> await session.destroy() + >>> await session.disconnect() >>> await client.stop() >>> # Or connect to an existing server @@ -348,10 +348,10 @@ async def stop(self) -> list["StopError"]: for session in sessions_to_destroy: try: - await session.destroy() + await session.disconnect() except Exception as e: errors.append( - StopError(message=f"Failed to destroy session {session.session_id}: {e}") + StopError(message=f"Failed to disconnect session {session.session_id}: {e}") ) # Close client @@ -935,7 +935,7 @@ async def delete_session(self, session_id: str) -> None: Permanently delete a session and all its data from disk, including conversation history, planning state, and artifacts. - Unlike :meth:`CopilotSession.destroy`, which only releases in-memory + Unlike :meth:`CopilotSession.disconnect`, which only releases in-memory resources and preserves session data for later resumption, this method is irreversible. The session cannot be resumed after deletion. diff --git a/python/copilot/session.py b/python/copilot/session.py index 72c08530..87051110 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -118,7 +118,7 @@ async def send(self, options: MessageOptions) -> str: The message ID of the response, which can be used to correlate events. Raises: - Exception: If the session has been destroyed or the connection fails. + Exception: If the session has been disconnected or the connection fails. Example: >>> message_id = await session.send({ @@ -159,7 +159,7 @@ async def send_and_wait( Raises: TimeoutError: If the timeout is reached before session becomes idle. - Exception: If the session has been destroyed or the connection fails. + Exception: If the session has been disconnected or the connection fails. Example: >>> response = await session.send_and_wait({"prompt": "What is 2+2?"}) @@ -461,7 +461,7 @@ async def get_messages(self) -> list[SessionEvent]: A list of all session events in chronological order. Raises: - Exception: If the session has been destroyed or the connection fails. + Exception: If the session has been disconnected or the connection fails. Example: >>> events = await session.get_messages() @@ -474,9 +474,9 @@ async def get_messages(self) -> list[SessionEvent]: events_dicts = response["events"] return [session_event_from_dict(event_dict) for event_dict in events_dicts] - async def destroy(self) -> None: + async def disconnect(self) -> None: """ - Close this session and release all in-memory resources (event handlers, + Disconnect this session and release all in-memory resources (event handlers, tool handlers, permission handlers). Session state on disk (conversation history, planning state, artifacts) @@ -492,7 +492,7 @@ async def destroy(self) -> None: Example: >>> # Clean up when done — session can still be resumed later - >>> await session.destroy() + >>> await session.disconnect() """ await self._client.request("session.destroy", {"sessionId": self.session_id}) with self._event_handlers_lock: @@ -502,6 +502,34 @@ async def destroy(self) -> None: with self._permission_handler_lock: self._permission_handler = None + async def destroy(self) -> None: + """ + .. deprecated:: + Use :meth:`disconnect` instead. This method will be removed in a future release. + + Disconnect this session and release all in-memory resources. + Session data on disk is preserved for later resumption. + + Raises: + Exception: If the connection fails. + """ + import warnings + + warnings.warn( + "destroy() is deprecated, use disconnect() instead", + DeprecationWarning, + stacklevel=2, + ) + await self.disconnect() + + async def __aenter__(self) -> "CopilotSession": + """Enable use as an async context manager.""" + return self + + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + """Disconnect the session when exiting the context manager.""" + await self.disconnect() + async def abort(self) -> None: """ Abort the currently processing message in this session. @@ -510,7 +538,7 @@ async def abort(self) -> None: and can continue to be used for new messages. Raises: - Exception: If the session has been destroyed or the connection fails. + Exception: If the session has been disconnected or the connection fails. Example: >>> import asyncio From b760022909ec5cca0c265c94beee41bbdc51a17e Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Wed, 4 Mar 2026 19:30:10 -0800 Subject: [PATCH 3/5] chore: update all tests, scenarios, and docs to use disconnect() Migrate all test scenarios, e2e tests, READMEs, and documentation references from destroy()/Destroy() to disconnect()/Disconnect(). - 90 test scenario files across Go/Python/TypeScript/C# - 15 Node.js e2e test files - 8 Python e2e test files - 3 Go e2e test files - 1 .NET test file - READMEs and compatibility docs updated with new API reference - Agent docs updated with new method names - Reconnect scenario log messages updated to 'disconnected' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/agents/docs-maintenance.agent.md | 6 +++--- docs/compatibility.md | 3 ++- docs/guides/setup/scaling.md | 2 +- dotnet/test/SessionTests.cs | 2 +- go/README.md | 3 ++- go/internal/e2e/mcp_and_agents_test.go | 20 +++++++++---------- go/internal/e2e/session_test.go | 8 ++++---- go/internal/e2e/skills_test.go | 6 +++--- nodejs/README.md | 8 ++++++-- nodejs/test/e2e/agent_and_compact_rpc.test.ts | 12 +++++------ nodejs/test/e2e/ask_user.test.ts | 6 +++--- nodejs/test/e2e/client.test.ts | 2 +- nodejs/test/e2e/client_lifecycle.test.ts | 4 ++-- nodejs/test/e2e/error_resilience.test.ts | 12 +++++------ nodejs/test/e2e/event_fidelity.test.ts | 8 ++++---- nodejs/test/e2e/hooks.test.ts | 8 ++++---- nodejs/test/e2e/hooks_extended.test.ts | 10 +++++----- nodejs/test/e2e/mcp_and_agents.test.ts | 20 +++++++++---------- nodejs/test/e2e/permissions.test.ts | 18 ++++++++--------- nodejs/test/e2e/session.test.ts | 8 ++++---- nodejs/test/e2e/session_config.test.ts | 8 ++++---- nodejs/test/e2e/session_lifecycle.test.ts | 12 +++++------ nodejs/test/e2e/skills.test.ts | 6 +++--- nodejs/test/e2e/streaming_fidelity.test.ts | 4 ++-- nodejs/test/e2e/tool_results.test.ts | 6 +++--- python/e2e/test_agent_and_compact_rpc.py | 12 +++++------ python/e2e/test_ask_user.py | 6 +++--- python/e2e/test_client.py | 2 +- python/e2e/test_hooks.py | 8 ++++---- python/e2e/test_mcp_and_agents.py | 12 +++++------ python/e2e/test_permissions.py | 18 ++++++++--------- python/e2e/test_rpc.py | 6 +++--- python/e2e/test_session.py | 8 ++++---- python/e2e/test_skills.py | 6 +++--- test/scenarios/auth/byok-anthropic/go/main.go | 2 +- .../auth/byok-anthropic/python/main.py | 2 +- .../byok-anthropic/typescript/src/index.ts | 2 +- test/scenarios/auth/byok-azure/go/main.go | 2 +- test/scenarios/auth/byok-azure/python/main.py | 2 +- .../auth/byok-azure/typescript/src/index.ts | 2 +- test/scenarios/auth/byok-ollama/go/main.go | 2 +- .../scenarios/auth/byok-ollama/python/main.py | 2 +- .../auth/byok-ollama/typescript/src/index.ts | 2 +- test/scenarios/auth/byok-openai/go/main.go | 2 +- .../scenarios/auth/byok-openai/python/main.py | 2 +- .../auth/byok-openai/typescript/src/index.ts | 2 +- test/scenarios/auth/gh-app/go/main.go | 2 +- test/scenarios/auth/gh-app/python/main.py | 2 +- .../auth/gh-app/typescript/src/index.ts | 2 +- .../bundling/app-backend-to-server/go/main.go | 2 +- .../app-backend-to-server/python/main.py | 2 +- .../typescript/src/index.ts | 2 +- .../bundling/app-direct-server/go/main.go | 2 +- .../bundling/app-direct-server/python/main.py | 2 +- .../app-direct-server/typescript/src/index.ts | 2 +- .../bundling/container-proxy/go/main.go | 2 +- .../bundling/container-proxy/python/main.py | 2 +- .../container-proxy/typescript/src/index.ts | 2 +- .../bundling/fully-bundled/go/main.go | 2 +- .../bundling/fully-bundled/python/main.py | 2 +- .../fully-bundled/typescript/src/index.ts | 2 +- test/scenarios/callbacks/hooks/go/main.go | 2 +- test/scenarios/callbacks/hooks/python/main.py | 2 +- .../callbacks/hooks/typescript/src/index.ts | 2 +- .../callbacks/permissions/go/main.go | 2 +- .../callbacks/permissions/python/main.py | 2 +- .../permissions/typescript/src/index.ts | 2 +- .../scenarios/callbacks/user-input/go/main.go | 2 +- .../callbacks/user-input/python/main.py | 2 +- .../user-input/typescript/src/index.ts | 2 +- test/scenarios/modes/default/go/main.go | 2 +- test/scenarios/modes/default/python/main.py | 2 +- .../modes/default/typescript/src/index.ts | 2 +- test/scenarios/modes/minimal/go/main.go | 2 +- test/scenarios/modes/minimal/python/main.py | 2 +- .../modes/minimal/typescript/src/index.ts | 2 +- test/scenarios/prompts/attachments/go/main.go | 2 +- .../prompts/attachments/python/main.py | 2 +- .../attachments/typescript/src/index.ts | 2 +- .../prompts/reasoning-effort/go/main.go | 2 +- .../prompts/reasoning-effort/python/main.py | 2 +- .../reasoning-effort/typescript/src/index.ts | 2 +- .../prompts/system-message/go/main.go | 2 +- .../prompts/system-message/python/main.py | 2 +- .../system-message/typescript/src/index.ts | 2 +- .../sessions/concurrent-sessions/go/main.go | 4 ++-- .../concurrent-sessions/python/main.py | 2 +- .../typescript/src/index.ts | 2 +- .../sessions/infinite-sessions/go/main.go | 2 +- .../sessions/infinite-sessions/python/main.py | 2 +- .../infinite-sessions/typescript/src/index.ts | 2 +- .../sessions/multi-user-short-lived/README.md | 2 +- .../sessions/session-resume/go/main.go | 4 ++-- .../sessions/session-resume/python/main.py | 4 ++-- .../session-resume/typescript/src/index.ts | 4 ++-- test/scenarios/sessions/streaming/go/main.go | 2 +- .../sessions/streaming/python/main.py | 2 +- .../streaming/typescript/src/index.ts | 2 +- test/scenarios/tools/custom-agents/go/main.go | 2 +- .../tools/custom-agents/python/main.py | 2 +- .../custom-agents/typescript/src/index.ts | 2 +- test/scenarios/tools/mcp-servers/go/main.go | 2 +- .../tools/mcp-servers/python/main.py | 2 +- .../tools/mcp-servers/typescript/src/index.ts | 2 +- test/scenarios/tools/no-tools/go/main.go | 2 +- test/scenarios/tools/no-tools/python/main.py | 2 +- .../tools/no-tools/typescript/src/index.ts | 2 +- test/scenarios/tools/skills/go/main.go | 2 +- test/scenarios/tools/skills/python/main.py | 2 +- .../tools/skills/typescript/src/index.ts | 2 +- .../scenarios/tools/tool-filtering/go/main.go | 2 +- .../tools/tool-filtering/python/main.py | 2 +- .../tool-filtering/typescript/src/index.ts | 2 +- .../tools/virtual-filesystem/go/main.go | 2 +- .../tools/virtual-filesystem/python/main.py | 2 +- .../typescript/src/index.ts | 2 +- test/scenarios/transport/reconnect/README.md | 4 ++-- .../transport/reconnect/csharp/Program.cs | 4 ++-- test/scenarios/transport/reconnect/go/main.go | 8 ++++---- .../transport/reconnect/python/main.py | 8 ++++---- .../reconnect/typescript/src/index.ts | 8 ++++---- test/scenarios/transport/stdio/go/main.go | 2 +- test/scenarios/transport/stdio/python/main.py | 2 +- .../transport/stdio/typescript/src/index.ts | 2 +- test/scenarios/transport/tcp/go/main.go | 2 +- test/scenarios/transport/tcp/python/main.py | 2 +- .../transport/tcp/typescript/src/index.ts | 2 +- 127 files changed, 251 insertions(+), 245 deletions(-) diff --git a/.github/agents/docs-maintenance.agent.md b/.github/agents/docs-maintenance.agent.md index 9b605c26..9b97fecf 100644 --- a/.github/agents/docs-maintenance.agent.md +++ b/.github/agents/docs-maintenance.agent.md @@ -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 @@ -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 @@ -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 diff --git a/docs/compatibility.md b/docs/compatibility.md index 268c077a..bfd17915 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -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 | diff --git a/docs/guides/setup/scaling.md b/docs/guides/setup/scaling.md index 5b74dee5..974276e5 100644 --- a/docs/guides/setup/scaling.md +++ b/docs/guides/setup/scaling.md @@ -412,7 +412,7 @@ class SessionManager { private async evictOldestSession(): Promise { const [oldestId] = this.activeSessions.keys(); const session = this.activeSessions.get(oldestId)!; - // Session state is persisted automatically — safe to destroy + // Session state is persisted automatically — safe to disconnect await session.disconnect(); this.activeSessions.delete(oldestId); } diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index e4b13fff..5391ee77 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -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" }); diff --git a/go/README.md b/go/README.md index 1ba496aa..0411a74f 100644 --- a/go/README.md +++ b/go/README.md @@ -168,7 +168,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 diff --git a/go/internal/e2e/mcp_and_agents_test.go b/go/internal/e2e/mcp_and_agents_test.go index 0f49a05c..079d26e9 100644 --- a/go/internal/e2e/mcp_and_agents_test.go +++ b/go/internal/e2e/mcp_and_agents_test.go @@ -55,7 +55,7 @@ func TestMCPServers(t *testing.T) { t.Errorf("Expected message to contain '4', got: %v", message.Data.Content) } - session.Destroy() + session.Disconnect() }) t.Run("accept MCP server config on resume", func(t *testing.T) { @@ -104,7 +104,7 @@ func TestMCPServers(t *testing.T) { t.Errorf("Expected message to contain '6', got: %v", message.Data.Content) } - session2.Destroy() + session2.Disconnect() }) t.Run("should pass literal env values to MCP server subprocess", func(t *testing.T) { @@ -150,7 +150,7 @@ func TestMCPServers(t *testing.T) { t.Errorf("Expected message to contain 'hunter2', got: %v", message.Data.Content) } - session.Destroy() + session.Disconnect() }) t.Run("handle multiple MCP servers", func(t *testing.T) { @@ -183,7 +183,7 @@ func TestMCPServers(t *testing.T) { t.Error("Expected non-empty session ID") } - session.Destroy() + session.Disconnect() }) } @@ -235,7 +235,7 @@ func TestCustomAgents(t *testing.T) { t.Errorf("Expected message to contain '10', got: %v", message.Data.Content) } - session.Destroy() + session.Disconnect() }) t.Run("accept custom agent config on resume", func(t *testing.T) { @@ -284,7 +284,7 @@ func TestCustomAgents(t *testing.T) { t.Errorf("Expected message to contain '12', got: %v", message.Data.Content) } - session2.Destroy() + session2.Disconnect() }) t.Run("handle custom agent with tools", func(t *testing.T) { @@ -314,7 +314,7 @@ func TestCustomAgents(t *testing.T) { t.Error("Expected non-empty session ID") } - session.Destroy() + session.Disconnect() }) t.Run("handle custom agent with MCP servers", func(t *testing.T) { @@ -349,7 +349,7 @@ func TestCustomAgents(t *testing.T) { t.Error("Expected non-empty session ID") } - session.Destroy() + session.Disconnect() }) t.Run("handle multiple custom agents", func(t *testing.T) { @@ -386,7 +386,7 @@ func TestCustomAgents(t *testing.T) { t.Error("Expected non-empty session ID") } - session.Destroy() + session.Disconnect() }) } @@ -445,6 +445,6 @@ func TestCombinedConfiguration(t *testing.T) { t.Errorf("Expected message to contain '14', got: %v", message.Data.Content) } - session.Destroy() + session.Disconnect() }) } diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go index f04307c2..24388ad2 100644 --- a/go/internal/e2e/session_test.go +++ b/go/internal/e2e/session_test.go @@ -15,7 +15,7 @@ func TestSession(t *testing.T) { client := ctx.NewClient() t.Cleanup(func() { client.ForceStop() }) - t.Run("should create and destroy sessions", func(t *testing.T) { + t.Run("should create and disconnect sessions", func(t *testing.T) { ctx.ConfigureForTest(t) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Model: "fake-test-model"}) @@ -45,13 +45,13 @@ func TestSession(t *testing.T) { t.Errorf("Expected selectedModel to be 'fake-test-model', got %v", messages[0].Data.SelectedModel) } - if err := session.Destroy(); err != nil { - t.Fatalf("Failed to destroy session: %v", err) + if err := session.Disconnect(); err != nil { + t.Fatalf("Failed to disconnect session: %v", err) } _, err = session.GetMessages(t.Context()) if err == nil || !strings.Contains(err.Error(), "not found") { - t.Errorf("Expected GetMessages to fail with 'not found' after destroy, got %v", err) + t.Errorf("Expected GetMessages to fail with 'not found' after disconnect, got %v", err) } }) diff --git a/go/internal/e2e/skills_test.go b/go/internal/e2e/skills_test.go index 10cd5002..524280fd 100644 --- a/go/internal/e2e/skills_test.go +++ b/go/internal/e2e/skills_test.go @@ -76,7 +76,7 @@ func TestSkills(t *testing.T) { t.Errorf("Expected message to contain skill marker '%s', got: %v", skillMarker, message.Data.Content) } - session.Destroy() + session.Disconnect() }) t.Run("should not apply skill when disabled via disabledSkills", func(t *testing.T) { @@ -105,7 +105,7 @@ func TestSkills(t *testing.T) { t.Errorf("Expected message to NOT contain skill marker '%s' when disabled, got: %v", skillMarker, *message.Data.Content) } - session.Destroy() + session.Disconnect() }) t.Run("should apply skill on session resume with skillDirectories", func(t *testing.T) { @@ -154,6 +154,6 @@ func TestSkills(t *testing.T) { t.Errorf("Expected message to contain skill marker '%s' after resume, got: %v", skillMarker, message2.Data.Content) } - session2.Destroy() + session2.Disconnect() }) } diff --git a/nodejs/README.md b/nodejs/README.md index 64f39674..9c91aef2 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -265,9 +265,13 @@ Abort the currently processing message in this session. Get all events/messages from this session. -##### `destroy(): Promise` +##### `disconnect(): Promise` -Destroy the session and free resources. +Disconnect the session and free resources. Session data on disk is preserved for later resumption. + +##### `destroy(): Promise` *(deprecated)* + +Deprecated — use `disconnect()` instead. --- diff --git a/nodejs/test/e2e/agent_and_compact_rpc.test.ts b/nodejs/test/e2e/agent_and_compact_rpc.test.ts index 47fc8322..336cd69b 100644 --- a/nodejs/test/e2e/agent_and_compact_rpc.test.ts +++ b/nodejs/test/e2e/agent_and_compact_rpc.test.ts @@ -40,7 +40,7 @@ describe("Agent Selection RPC", async () => { expect(result.agents[0].description).toBe("A test agent"); expect(result.agents[1].name).toBe("another-agent"); - await session.destroy(); + await session.disconnect(); }); it("should return null when no agent is selected", async () => { @@ -61,7 +61,7 @@ describe("Agent Selection RPC", async () => { const result = await session.rpc.agent.getCurrent(); expect(result.agent).toBeNull(); - await session.destroy(); + await session.disconnect(); }); it("should select and get current agent", async () => { @@ -90,7 +90,7 @@ describe("Agent Selection RPC", async () => { expect(currentResult.agent).not.toBeNull(); expect(currentResult.agent!.name).toBe("test-agent"); - await session.destroy(); + await session.disconnect(); }); it("should deselect current agent", async () => { @@ -116,7 +116,7 @@ describe("Agent Selection RPC", async () => { const currentResult = await session.rpc.agent.getCurrent(); expect(currentResult.agent).toBeNull(); - await session.destroy(); + await session.disconnect(); }); it("should return empty list when no custom agents configured", async () => { @@ -125,7 +125,7 @@ describe("Agent Selection RPC", async () => { const result = await session.rpc.agent.list(); expect(result.agents).toEqual([]); - await session.destroy(); + await session.disconnect(); }); }); @@ -144,6 +144,6 @@ describe("Session Compact RPC", async () => { expect(typeof result.tokensRemoved).toBe("number"); expect(typeof result.messagesRemoved).toBe("number"); - await session.destroy(); + await session.disconnect(); }, 60000); }); diff --git a/nodejs/test/e2e/ask_user.test.ts b/nodejs/test/e2e/ask_user.test.ts index c58daa00..deb0d788 100644 --- a/nodejs/test/e2e/ask_user.test.ts +++ b/nodejs/test/e2e/ask_user.test.ts @@ -38,7 +38,7 @@ describe("User input (ask_user)", async () => { // The request should have a question expect(userInputRequests.some((req) => req.question && req.question.length > 0)).toBe(true); - await session.destroy(); + await session.disconnect(); }); it("should receive choices in user input request", async () => { @@ -69,7 +69,7 @@ describe("User input (ask_user)", async () => { ); expect(requestWithChoices).toBeDefined(); - await session.destroy(); + await session.disconnect(); }); it("should handle freeform user input response", async () => { @@ -99,6 +99,6 @@ describe("User input (ask_user)", async () => { // (This is a soft check since the model may paraphrase) expect(response).toBeDefined(); - await session.destroy(); + await session.disconnect(); }); }); diff --git a/nodejs/test/e2e/client.test.ts b/nodejs/test/e2e/client.test.ts index c7539fc0..9d71ee72 100644 --- a/nodejs/test/e2e/client.test.ts +++ b/nodejs/test/e2e/client.test.ts @@ -62,7 +62,7 @@ describe("Client", () => { const errors = await client.stop(); expect(errors.length).toBeGreaterThan(0); - expect(errors[0].message).toContain("Failed to destroy session"); + expect(errors[0].message).toContain("Failed to disconnect session"); }); it("should forceStop without cleanup", async () => { diff --git a/nodejs/test/e2e/client_lifecycle.test.ts b/nodejs/test/e2e/client_lifecycle.test.ts index 1e6f451e..beb65432 100644 --- a/nodejs/test/e2e/client_lifecycle.test.ts +++ b/nodejs/test/e2e/client_lifecycle.test.ts @@ -20,7 +20,7 @@ describe("Client Lifecycle", async () => { const lastSessionId = await client.getLastSessionId(); expect(lastSessionId).toBe(session.sessionId); - await session.destroy(); + await session.disconnect(); }); it("should return undefined for getLastSessionId with no sessions", async () => { @@ -49,7 +49,7 @@ describe("Client Lifecycle", async () => { expect(sessionEvents.length).toBeGreaterThan(0); } - await session.destroy(); + await session.disconnect(); } finally { unsubscribe(); } diff --git a/nodejs/test/e2e/error_resilience.test.ts b/nodejs/test/e2e/error_resilience.test.ts index bf908560..183ea118 100644 --- a/nodejs/test/e2e/error_resilience.test.ts +++ b/nodejs/test/e2e/error_resilience.test.ts @@ -9,16 +9,16 @@ import { createSdkTestContext } from "./harness/sdkTestContext"; describe("Error Resilience", async () => { const { copilotClient: client } = await createSdkTestContext(); - it("should throw when sending to destroyed session", async () => { + it("should throw when sending to disconnected session", async () => { const session = await client.createSession({ onPermissionRequest: approveAll }); - await session.destroy(); + await session.disconnect(); await expect(session.sendAndWait({ prompt: "Hello" })).rejects.toThrow(); }); - it("should throw when getting messages from destroyed session", async () => { + it("should throw when getting messages from disconnected session", async () => { const session = await client.createSession({ onPermissionRequest: approveAll }); - await session.destroy(); + await session.disconnect(); await expect(session.getMessages()).rejects.toThrow(); }); @@ -31,8 +31,8 @@ describe("Error Resilience", async () => { // Second abort should not throw await session.abort(); - // Session should still be destroyable - await session.destroy(); + // Session should still be disconnectable + await session.disconnect(); }); it("should throw when resuming non-existent session", async () => { diff --git a/nodejs/test/e2e/event_fidelity.test.ts b/nodejs/test/e2e/event_fidelity.test.ts index a9e9b77a..7cd65b6f 100644 --- a/nodejs/test/e2e/event_fidelity.test.ts +++ b/nodejs/test/e2e/event_fidelity.test.ts @@ -39,7 +39,7 @@ describe("Event Fidelity", async () => { const idleIdx = types.lastIndexOf("session.idle"); expect(idleIdx).toBe(types.length - 1); - await session.destroy(); + await session.disconnect(); }); it("should include valid fields on all events", async () => { @@ -74,7 +74,7 @@ describe("Event Fidelity", async () => { expect(assistantEvent?.data.messageId).toBeDefined(); expect(assistantEvent?.data.content).toBeDefined(); - await session.destroy(); + await session.disconnect(); }); it("should emit tool execution events with correct fields", async () => { @@ -106,7 +106,7 @@ describe("Event Fidelity", async () => { const firstComplete = toolCompletes[0]!; expect(firstComplete.data.toolCallId).toBeDefined(); - await session.destroy(); + await session.disconnect(); }); it("should emit assistant.message with messageId", async () => { @@ -129,6 +129,6 @@ describe("Event Fidelity", async () => { expect(typeof msg.data.messageId).toBe("string"); expect(msg.data.content).toContain("pong"); - await session.destroy(); + await session.disconnect(); }); }); diff --git a/nodejs/test/e2e/hooks.test.ts b/nodejs/test/e2e/hooks.test.ts index b7d8d4dc..9743d91f 100644 --- a/nodejs/test/e2e/hooks.test.ts +++ b/nodejs/test/e2e/hooks.test.ts @@ -45,7 +45,7 @@ describe("Session hooks", async () => { // Should have received the tool name expect(preToolUseInputs.some((input) => input.toolName)).toBe(true); - await session.destroy(); + await session.disconnect(); }); it("should invoke postToolUse hook after model runs a tool", async () => { @@ -76,7 +76,7 @@ describe("Session hooks", async () => { expect(postToolUseInputs.some((input) => input.toolName)).toBe(true); expect(postToolUseInputs.some((input) => input.toolResult !== undefined)).toBe(true); - await session.destroy(); + await session.disconnect(); }); it("should invoke both preToolUse and postToolUse hooks for a single tool call", async () => { @@ -113,7 +113,7 @@ describe("Session hooks", async () => { const commonTool = preToolNames.find((name) => postToolNames.includes(name)); expect(commonTool).toBeDefined(); - await session.destroy(); + await session.disconnect(); }); it("should deny tool execution when preToolUse returns deny", async () => { @@ -145,6 +145,6 @@ describe("Session hooks", async () => { // At minimum, we verify the hook was invoked expect(response).toBeDefined(); - await session.destroy(); + await session.disconnect(); }); }); diff --git a/nodejs/test/e2e/hooks_extended.test.ts b/nodejs/test/e2e/hooks_extended.test.ts index b9735663..9b12c441 100644 --- a/nodejs/test/e2e/hooks_extended.test.ts +++ b/nodejs/test/e2e/hooks_extended.test.ts @@ -37,7 +37,7 @@ describe("Extended session hooks", async () => { expect(sessionStartInputs[0].timestamp).toBeGreaterThan(0); expect(sessionStartInputs[0].cwd).toBeDefined(); - await session.destroy(); + await session.disconnect(); }); it("should invoke onUserPromptSubmitted hook when sending a message", async () => { @@ -62,10 +62,10 @@ describe("Extended session hooks", async () => { expect(userPromptInputs[0].timestamp).toBeGreaterThan(0); expect(userPromptInputs[0].cwd).toBeDefined(); - await session.destroy(); + await session.disconnect(); }); - it("should invoke onSessionEnd hook when session is destroyed", async () => { + it("should invoke onSessionEnd hook when session is disconnected", async () => { const sessionEndInputs: SessionEndHookInput[] = []; const session = await client.createSession({ @@ -82,7 +82,7 @@ describe("Extended session hooks", async () => { prompt: "Say hi", }); - await session.destroy(); + await session.disconnect(); // Wait briefly for async hook await new Promise((resolve) => setTimeout(resolve, 100)); @@ -120,6 +120,6 @@ describe("Extended session hooks", async () => { // If the hook did fire, the assertions inside it would have run. expect(session.sessionId).toBeDefined(); - await session.destroy(); + await session.disconnect(); }); }); diff --git a/nodejs/test/e2e/mcp_and_agents.test.ts b/nodejs/test/e2e/mcp_and_agents.test.ts index cc626e32..28ebf28b 100644 --- a/nodejs/test/e2e/mcp_and_agents.test.ts +++ b/nodejs/test/e2e/mcp_and_agents.test.ts @@ -40,7 +40,7 @@ describe("MCP Servers and Custom Agents", async () => { }); expect(message?.data.content).toContain("4"); - await session.destroy(); + await session.disconnect(); }); it("should accept MCP server configuration on session resume", async () => { @@ -71,7 +71,7 @@ describe("MCP Servers and Custom Agents", async () => { }); expect(message?.data.content).toContain("6"); - await session2.destroy(); + await session2.disconnect(); }); it("should handle multiple MCP servers", async () => { @@ -96,7 +96,7 @@ describe("MCP Servers and Custom Agents", async () => { }); expect(session.sessionId).toBeDefined(); - await session.destroy(); + await session.disconnect(); }); it("should pass literal env values to MCP server subprocess", async () => { @@ -122,7 +122,7 @@ describe("MCP Servers and Custom Agents", async () => { }); expect(message?.data.content).toContain("hunter2"); - await session.destroy(); + await session.disconnect(); }); }); @@ -151,7 +151,7 @@ describe("MCP Servers and Custom Agents", async () => { }); expect(message?.data.content).toContain("10"); - await session.destroy(); + await session.disconnect(); }); it("should accept custom agent configuration on session resume", async () => { @@ -182,7 +182,7 @@ describe("MCP Servers and Custom Agents", async () => { }); expect(message?.data.content).toContain("12"); - await session2.destroy(); + await session2.disconnect(); }); it("should handle custom agent with tools configuration", async () => { @@ -203,7 +203,7 @@ describe("MCP Servers and Custom Agents", async () => { }); expect(session.sessionId).toBeDefined(); - await session.destroy(); + await session.disconnect(); }); it("should handle custom agent with MCP servers", async () => { @@ -230,7 +230,7 @@ describe("MCP Servers and Custom Agents", async () => { }); expect(session.sessionId).toBeDefined(); - await session.destroy(); + await session.disconnect(); }); it("should handle multiple custom agents", async () => { @@ -256,7 +256,7 @@ describe("MCP Servers and Custom Agents", async () => { }); expect(session.sessionId).toBeDefined(); - await session.destroy(); + await session.disconnect(); }); }); @@ -293,7 +293,7 @@ describe("MCP Servers and Custom Agents", async () => { }); expect(message?.data.content).toContain("14"); - await session.destroy(); + await session.disconnect(); }); }); }); diff --git a/nodejs/test/e2e/permissions.test.ts b/nodejs/test/e2e/permissions.test.ts index ea23bc07..2203e34a 100644 --- a/nodejs/test/e2e/permissions.test.ts +++ b/nodejs/test/e2e/permissions.test.ts @@ -39,7 +39,7 @@ describe("Permission callbacks", async () => { const writeRequests = permissionRequests.filter((req) => req.kind === "write"); expect(writeRequests.length).toBeGreaterThan(0); - await session.destroy(); + await session.disconnect(); }); it("should deny permission when handler returns denied", async () => { @@ -61,7 +61,7 @@ describe("Permission callbacks", async () => { const content = await readFile(testFile, "utf-8"); expect(content).toBe(originalContent); - await session.destroy(); + await session.disconnect(); }); it("should deny tool operations when handler explicitly denies", async () => { @@ -86,7 +86,7 @@ describe("Permission callbacks", async () => { expect(permissionDenied).toBe(true); - await session.destroy(); + await session.disconnect(); }); it("should deny tool operations when handler explicitly denies after resume", async () => { @@ -114,7 +114,7 @@ describe("Permission callbacks", async () => { expect(permissionDenied).toBe(true); - await session2.destroy(); + await session2.disconnect(); }); it("should work with approve-all permission handler", async () => { @@ -125,7 +125,7 @@ describe("Permission callbacks", async () => { }); expect(message?.data.content).toContain("4"); - await session.destroy(); + await session.disconnect(); }); it("should handle async permission handler", async () => { @@ -148,7 +148,7 @@ describe("Permission callbacks", async () => { expect(permissionRequests.length).toBeGreaterThan(0); - await session.destroy(); + await session.disconnect(); }); it("should resume session with permission handler", async () => { @@ -174,7 +174,7 @@ describe("Permission callbacks", async () => { // Should have permission requests from resumed session expect(permissionRequests.length).toBeGreaterThan(0); - await session2.destroy(); + await session2.disconnect(); }); it("should handle permission handler errors gracefully", async () => { @@ -191,7 +191,7 @@ describe("Permission callbacks", async () => { // Should handle the error and deny permission expect(message?.data.content?.toLowerCase()).toMatch(/fail|cannot|unable|permission/); - await session.destroy(); + await session.disconnect(); }); it("should receive toolCallId in permission requests", async () => { @@ -214,6 +214,6 @@ describe("Permission callbacks", async () => { expect(receivedToolCallId).toBe(true); - await session.destroy(); + await session.disconnect(); }); }); diff --git a/nodejs/test/e2e/session.test.ts b/nodejs/test/e2e/session.test.ts index 9d067a8e..d89d8192 100644 --- a/nodejs/test/e2e/session.test.ts +++ b/nodejs/test/e2e/session.test.ts @@ -8,7 +8,7 @@ import { getFinalAssistantMessage, getNextEventOfType } from "./harness/sdkTestH describe("Sessions", async () => { const { copilotClient: client, openAiEndpoint, homeDir, env } = await createSdkTestContext(); - it("should create and destroy sessions", async () => { + it("should create and disconnect sessions", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, model: "fake-test-model", @@ -22,7 +22,7 @@ describe("Sessions", async () => { }, ]); - await session.destroy(); + await session.disconnect(); await expect(() => session.getMessages()).rejects.toThrow(/Session not found/); }); @@ -155,8 +155,8 @@ describe("Sessions", async () => { ]); } - // All can be destroyed - await Promise.all([s1.destroy(), s2.destroy(), s3.destroy()]); + // All can be disconnected + await Promise.all([s1.disconnect(), s2.disconnect(), s3.disconnect()]); for (const s of [s1, s2, s3]) { await expect(() => s.getMessages()).rejects.toThrow(/Session not found/); } diff --git a/nodejs/test/e2e/session_config.test.ts b/nodejs/test/e2e/session_config.test.ts index ceb1f43f..2984c3c0 100644 --- a/nodejs/test/e2e/session_config.test.ts +++ b/nodejs/test/e2e/session_config.test.ts @@ -22,7 +22,7 @@ describe("Session Configuration", async () => { }); expect(assistantMessage?.data.content).toContain("subdirectory"); - await session.destroy(); + await session.disconnect(); }); it("should create session with custom provider config", async () => { @@ -37,9 +37,9 @@ describe("Session Configuration", async () => { expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); try { - await session.destroy(); + await session.disconnect(); } catch { - // destroy may fail since the provider is fake + // disconnect may fail since the provider is fake } }); @@ -54,6 +54,6 @@ describe("Session Configuration", async () => { }); // Just verify send doesn't throw — attachment support varies by runtime - await session.destroy(); + await session.disconnect(); }); }); diff --git a/nodejs/test/e2e/session_lifecycle.test.ts b/nodejs/test/e2e/session_lifecycle.test.ts index f41255cf..355f8998 100644 --- a/nodejs/test/e2e/session_lifecycle.test.ts +++ b/nodejs/test/e2e/session_lifecycle.test.ts @@ -26,8 +26,8 @@ describe("Session Lifecycle", async () => { expect(sessionIds).toContain(session1.sessionId); expect(sessionIds).toContain(session2.sessionId); - await session1.destroy(); - await session2.destroy(); + await session1.disconnect(); + await session2.disconnect(); }); it("should delete session permanently", async () => { @@ -44,7 +44,7 @@ describe("Session Lifecycle", async () => { const before = await client.listSessions(); expect(before.map((s) => s.sessionId)).toContain(sessionId); - await session.destroy(); + await session.disconnect(); await client.deleteSession(sessionId); // After delete, the session should not be in the list @@ -68,7 +68,7 @@ describe("Session Lifecycle", async () => { expect(types).toContain("user.message"); expect(types).toContain("assistant.message"); - await session.destroy(); + await session.disconnect(); }); it("should support multiple concurrent sessions", async () => { @@ -84,7 +84,7 @@ describe("Session Lifecycle", async () => { expect(msg1?.data.content).toContain("2"); expect(msg2?.data.content).toContain("6"); - await session1.destroy(); - await session2.destroy(); + await session1.disconnect(); + await session2.disconnect(); }); }); diff --git a/nodejs/test/e2e/skills.test.ts b/nodejs/test/e2e/skills.test.ts index 654f429a..a2173648 100644 --- a/nodejs/test/e2e/skills.test.ts +++ b/nodejs/test/e2e/skills.test.ts @@ -58,7 +58,7 @@ IMPORTANT: You MUST include the exact text "${SKILL_MARKER}" somewhere in EVERY expect(message?.data.content).toContain(SKILL_MARKER); - await session.destroy(); + await session.disconnect(); }); it("should not apply skill when disabled via disabledSkills", async () => { @@ -78,7 +78,7 @@ IMPORTANT: You MUST include the exact text "${SKILL_MARKER}" somewhere in EVERY expect(message?.data.content).not.toContain(SKILL_MARKER); - await session.destroy(); + await session.disconnect(); }); // Skipped because the underlying feature doesn't work correctly yet. @@ -118,7 +118,7 @@ IMPORTANT: You MUST include the exact text "${SKILL_MARKER}" somewhere in EVERY expect(message2?.data.content).toContain(SKILL_MARKER); - await session2.destroy(); + await session2.disconnect(); }); }); }); diff --git a/nodejs/test/e2e/streaming_fidelity.test.ts b/nodejs/test/e2e/streaming_fidelity.test.ts index a5a2ead2..04148ad8 100644 --- a/nodejs/test/e2e/streaming_fidelity.test.ts +++ b/nodejs/test/e2e/streaming_fidelity.test.ts @@ -43,7 +43,7 @@ describe("Streaming Fidelity", async () => { const lastAssistantIdx = types.lastIndexOf("assistant.message"); expect(firstDeltaIdx).toBeLessThan(lastAssistantIdx); - await session.destroy(); + await session.disconnect(); }); it("should not produce deltas when streaming is disabled", async () => { @@ -69,6 +69,6 @@ describe("Streaming Fidelity", async () => { const assistantEvents = events.filter((e) => e.type === "assistant.message"); expect(assistantEvents.length).toBeGreaterThanOrEqual(1); - await session.destroy(); + await session.disconnect(); }); }); diff --git a/nodejs/test/e2e/tool_results.test.ts b/nodejs/test/e2e/tool_results.test.ts index 88ebdb9a..66e71549 100644 --- a/nodejs/test/e2e/tool_results.test.ts +++ b/nodejs/test/e2e/tool_results.test.ts @@ -35,7 +35,7 @@ describe("Tool Results", async () => { const content = assistantMessage?.data.content ?? ""; expect(content).toMatch(/sunny|72/i); - await session.destroy(); + await session.disconnect(); }); it("should handle tool result with failure resultType", async () => { @@ -60,7 +60,7 @@ describe("Tool Results", async () => { const failureContent = assistantMessage?.data.content ?? ""; expect(failureContent).toMatch(/service is down/i); - await session.destroy(); + await session.disconnect(); }); it("should pass validated Zod parameters to tool handler", async () => { @@ -96,6 +96,6 @@ describe("Tool Results", async () => { expect(assistantMessage?.data.content).toContain("42"); - await session.destroy(); + await session.disconnect(); }); }); diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py index a960c842..cee6814f 100644 --- a/python/e2e/test_agent_and_compact_rpc.py +++ b/python/e2e/test_agent_and_compact_rpc.py @@ -46,7 +46,7 @@ async def test_should_list_available_custom_agents(self): assert result.agents[0].description == "A test agent" assert result.agents[1].name == "another-agent" - await session.destroy() + await session.disconnect() await client.stop() finally: await client.force_stop() @@ -75,7 +75,7 @@ async def test_should_return_null_when_no_agent_is_selected(self): result = await session.rpc.agent.get_current() assert result.agent is None - await session.destroy() + await session.disconnect() await client.stop() finally: await client.force_stop() @@ -114,7 +114,7 @@ async def test_should_select_and_get_current_agent(self): assert current_result.agent is not None assert current_result.agent.name == "test-agent" - await session.destroy() + await session.disconnect() await client.stop() finally: await client.force_stop() @@ -148,7 +148,7 @@ async def test_should_deselect_current_agent(self): current_result = await session.rpc.agent.get_current() assert current_result.agent is None - await session.destroy() + await session.disconnect() await client.stop() finally: await client.force_stop() @@ -167,7 +167,7 @@ async def test_should_return_empty_list_when_no_custom_agents_configured(self): result = await session.rpc.agent.list() assert result.agents == [] - await session.destroy() + await session.disconnect() await client.stop() finally: await client.force_stop() @@ -190,4 +190,4 @@ async def test_should_compact_session_history_after_messages(self, ctx: E2ETestC assert isinstance(result.tokens_removed, (int, float)) assert isinstance(result.messages_removed, (int, float)) - await session.destroy() + await session.disconnect() diff --git a/python/e2e/test_ask_user.py b/python/e2e/test_ask_user.py index f409e460..bddc062d 100644 --- a/python/e2e/test_ask_user.py +++ b/python/e2e/test_ask_user.py @@ -53,7 +53,7 @@ async def on_user_input_request(request, invocation): req.get("question") and len(req.get("question")) > 0 for req in user_input_requests ) - await session.destroy() + await session.disconnect() async def test_should_receive_choices_in_user_input_request(self, ctx: E2ETestContext): """Test that choices are received in user input request""" @@ -94,7 +94,7 @@ async def on_user_input_request(request, invocation): ) assert request_with_choices is not None - await session.destroy() + await session.disconnect() async def test_should_handle_freeform_user_input_response(self, ctx: E2ETestContext): """Test that freeform user input responses work""" @@ -132,4 +132,4 @@ async def on_user_input_request(request, invocation): # (This is a soft check since the model may paraphrase) assert response is not None - await session.destroy() + await session.disconnect() diff --git a/python/e2e/test_client.py b/python/e2e/test_client.py index cc5d31ac..780b1103 100644 --- a/python/e2e/test_client.py +++ b/python/e2e/test_client.py @@ -61,7 +61,7 @@ async def test_should_return_errors_on_failed_cleanup(self): errors = await client.stop() assert len(errors) > 0 - assert "Failed to destroy session" in errors[0].message + assert "Failed to disconnect session" in errors[0].message finally: await client.force_stop() diff --git a/python/e2e/test_hooks.py b/python/e2e/test_hooks.py index 8278fb33..c886c6e2 100644 --- a/python/e2e/test_hooks.py +++ b/python/e2e/test_hooks.py @@ -43,7 +43,7 @@ async def on_pre_tool_use(input_data, invocation): # Should have received the tool name assert any(inp.get("toolName") for inp in pre_tool_use_inputs) - await session.destroy() + await session.disconnect() async def test_should_invoke_posttooluse_hook_after_model_runs_a_tool( self, ctx: E2ETestContext @@ -77,7 +77,7 @@ async def on_post_tool_use(input_data, invocation): assert any(inp.get("toolName") for inp in post_tool_use_inputs) assert any(inp.get("toolResult") is not None for inp in post_tool_use_inputs) - await session.destroy() + await session.disconnect() async def test_should_invoke_both_pretooluse_and_posttooluse_hooks_for_a_single_tool_call( self, ctx: E2ETestContext @@ -118,7 +118,7 @@ async def on_post_tool_use(input_data, invocation): common_tool = next((name for name in pre_tool_names if name in post_tool_names), None) assert common_tool is not None - await session.destroy() + await session.disconnect() async def test_should_deny_tool_execution_when_pretooluse_returns_deny( self, ctx: E2ETestContext @@ -153,4 +153,4 @@ async def on_pre_tool_use(input_data, invocation): # At minimum, we verify the hook was invoked assert response is not None - await session.destroy() + await session.disconnect() diff --git a/python/e2e/test_mcp_and_agents.py b/python/e2e/test_mcp_and_agents.py index b29a5482..fd99cc2c 100644 --- a/python/e2e/test_mcp_and_agents.py +++ b/python/e2e/test_mcp_and_agents.py @@ -43,7 +43,7 @@ async def test_should_accept_mcp_server_configuration_on_session_create( assert message is not None assert "4" in message.data.content - await session.destroy() + await session.disconnect() async def test_should_accept_mcp_server_configuration_on_session_resume( self, ctx: E2ETestContext @@ -77,7 +77,7 @@ async def test_should_accept_mcp_server_configuration_on_session_resume( assert message is not None assert "6" in message.data.content - await session2.destroy() + await session2.disconnect() async def test_should_pass_literal_env_values_to_mcp_server_subprocess( self, ctx: E2ETestContext @@ -112,7 +112,7 @@ async def test_should_pass_literal_env_values_to_mcp_server_subprocess( assert message is not None assert "hunter2" in message.data.content - await session.destroy() + await session.disconnect() class TestCustomAgents: @@ -141,7 +141,7 @@ async def test_should_accept_custom_agent_configuration_on_session_create( assert message is not None assert "10" in message.data.content - await session.destroy() + await session.disconnect() async def test_should_accept_custom_agent_configuration_on_session_resume( self, ctx: E2ETestContext @@ -178,7 +178,7 @@ async def test_should_accept_custom_agent_configuration_on_session_resume( assert message is not None assert "12" in message.data.content - await session2.destroy() + await session2.disconnect() class TestCombinedConfiguration: @@ -216,4 +216,4 @@ async def test_should_accept_both_mcp_servers_and_custom_agents(self, ctx: E2ETe message = await get_final_assistant_message(session) assert "14" in message.data.content - await session.destroy() + await session.disconnect() diff --git a/python/e2e/test_permissions.py b/python/e2e/test_permissions.py index c116053b..722ddc33 100644 --- a/python/e2e/test_permissions.py +++ b/python/e2e/test_permissions.py @@ -42,7 +42,7 @@ def on_permission_request( write_requests = [req for req in permission_requests if req.get("kind") == "write"] assert len(write_requests) > 0 - await session.destroy() + await session.disconnect() async def test_should_deny_permission_when_handler_returns_denied(self, ctx: E2ETestContext): """Test denying permissions""" @@ -66,7 +66,7 @@ def on_permission_request( content = read_file(ctx.work_dir, "protected.txt") assert content == original_content - await session.destroy() + await session.disconnect() async def test_should_deny_tool_operations_when_handler_explicitly_denies( self, ctx: E2ETestContext @@ -101,7 +101,7 @@ def on_event(event): assert len(denied_events) > 0 - await session.destroy() + await session.disconnect() async def test_should_deny_tool_operations_when_handler_explicitly_denies_after_resume( self, ctx: E2ETestContext @@ -141,7 +141,7 @@ def on_event(event): assert len(denied_events) > 0 - await session2.destroy() + await session2.disconnect() async def test_should_work_with_approve_all_permission_handler(self, ctx: E2ETestContext): """Test that sessions work with approve-all permission handler""" @@ -154,7 +154,7 @@ async def test_should_work_with_approve_all_permission_handler(self, ctx: E2ETes assert message is not None assert "4" in message.data.content - await session.destroy() + await session.disconnect() async def test_should_handle_async_permission_handler(self, ctx: E2ETestContext): """Test async permission handler""" @@ -174,7 +174,7 @@ async def on_permission_request( assert len(permission_requests) > 0 - await session.destroy() + await session.disconnect() async def test_should_resume_session_with_permission_handler(self, ctx: E2ETestContext): """Test resuming session with permission handler""" @@ -203,7 +203,7 @@ def on_permission_request( # Should have permission requests from resumed session assert len(permission_requests) > 0 - await session2.destroy() + await session2.disconnect() async def test_should_handle_permission_handler_errors_gracefully(self, ctx: E2ETestContext): """Test that permission handler errors are handled gracefully""" @@ -224,7 +224,7 @@ def on_permission_request( content_lower = message.data.content.lower() assert any(word in content_lower for word in ["fail", "cannot", "unable", "permission"]) - await session.destroy() + await session.disconnect() async def test_should_receive_toolcallid_in_permission_requests(self, ctx: E2ETestContext): """Test that toolCallId is included in permission requests""" @@ -246,4 +246,4 @@ def on_permission_request( assert received_tool_call_id - await session.destroy() + await session.disconnect() diff --git a/python/e2e/test_rpc.py b/python/e2e/test_rpc.py index 240cd373..1b455d63 100644 --- a/python/e2e/test_rpc.py +++ b/python/e2e/test_rpc.py @@ -138,7 +138,7 @@ async def test_get_and_set_session_mode(self): ) assert interactive_result.mode == Mode.INTERACTIVE - await session.destroy() + await session.disconnect() await client.stop() finally: await client.force_stop() @@ -178,7 +178,7 @@ async def test_read_update_and_delete_plan(self): assert after_delete.exists is False assert after_delete.content is None - await session.destroy() + await session.disconnect() await client.stop() finally: await client.force_stop() @@ -228,7 +228,7 @@ async def test_create_list_and_read_workspace_files(self): assert "test.txt" in after_nested.files assert any("nested.txt" in f for f in after_nested.files) - await session.destroy() + await session.disconnect() await client.stop() finally: await client.force_stop() diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index 47cb1b5a..7d39be52 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -13,7 +13,7 @@ class TestSessions: - async def test_should_create_and_destroy_sessions(self, ctx: E2ETestContext): + async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext): session = await ctx.client.create_session( {"model": "fake-test-model", "on_permission_request": PermissionHandler.approve_all} ) @@ -25,7 +25,7 @@ async def test_should_create_and_destroy_sessions(self, ctx: E2ETestContext): assert messages[0].data.session_id == session.session_id assert messages[0].data.selected_model == "fake-test-model" - await session.destroy() + await session.disconnect() with pytest.raises(Exception, match="Session not found"): await session.get_messages() @@ -148,8 +148,8 @@ async def test_should_handle_multiple_concurrent_sessions(self, ctx: E2ETestCont assert messages[0].type.value == "session.start" assert messages[0].data.session_id == s.session_id - # All can be destroyed - await asyncio.gather(s1.destroy(), s2.destroy(), s3.destroy()) + # All can be disconnected + await asyncio.gather(s1.disconnect(), s2.disconnect(), s3.disconnect()) for s in [s1, s2, s3]: with pytest.raises(Exception, match="Session not found"): await s.get_messages() diff --git a/python/e2e/test_skills.py b/python/e2e/test_skills.py index 10d32695..166840e5 100644 --- a/python/e2e/test_skills.py +++ b/python/e2e/test_skills.py @@ -69,7 +69,7 @@ async def test_should_load_and_apply_skill_from_skilldirectories(self, ctx: E2ET assert message is not None assert SKILL_MARKER in message.data.content - await session.destroy() + await session.disconnect() async def test_should_not_apply_skill_when_disabled_via_disabledskills( self, ctx: E2ETestContext @@ -91,7 +91,7 @@ async def test_should_not_apply_skill_when_disabled_via_disabledskills( assert message is not None assert SKILL_MARKER not in message.data.content - await session.destroy() + await session.disconnect() @pytest.mark.skip( reason="See the big comment around the equivalent test in the Node SDK. " @@ -130,4 +130,4 @@ async def test_should_apply_skill_on_session_resume_with_skilldirectories( assert message2 is not None assert SKILL_MARKER in message2.data.content - await session2.destroy() + await session2.disconnect() diff --git a/test/scenarios/auth/byok-anthropic/go/main.go b/test/scenarios/auth/byok-anthropic/go/main.go index a42f90b8..048d20f6 100644 --- a/test/scenarios/auth/byok-anthropic/go/main.go +++ b/test/scenarios/auth/byok-anthropic/go/main.go @@ -49,7 +49,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "What is the capital of France?", diff --git a/test/scenarios/auth/byok-anthropic/python/main.py b/test/scenarios/auth/byok-anthropic/python/main.py index 7f5e5834..e50a33c1 100644 --- a/test/scenarios/auth/byok-anthropic/python/main.py +++ b/test/scenarios/auth/byok-anthropic/python/main.py @@ -40,7 +40,7 @@ async def main(): if response: print(response.data.content) - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/auth/byok-anthropic/typescript/src/index.ts b/test/scenarios/auth/byok-anthropic/typescript/src/index.ts index bd5f30dd..a7f460d8 100644 --- a/test/scenarios/auth/byok-anthropic/typescript/src/index.ts +++ b/test/scenarios/auth/byok-anthropic/typescript/src/index.ts @@ -36,7 +36,7 @@ async function main() { console.log(response.data.content); } - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/auth/byok-azure/go/main.go b/test/scenarios/auth/byok-azure/go/main.go index 8d385076..03f3b9dc 100644 --- a/test/scenarios/auth/byok-azure/go/main.go +++ b/test/scenarios/auth/byok-azure/go/main.go @@ -53,7 +53,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "What is the capital of France?", diff --git a/test/scenarios/auth/byok-azure/python/main.py b/test/scenarios/auth/byok-azure/python/main.py index 5376cac2..89f37178 100644 --- a/test/scenarios/auth/byok-azure/python/main.py +++ b/test/scenarios/auth/byok-azure/python/main.py @@ -44,7 +44,7 @@ async def main(): if response: print(response.data.content) - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/auth/byok-azure/typescript/src/index.ts b/test/scenarios/auth/byok-azure/typescript/src/index.ts index 450742f8..397a0a18 100644 --- a/test/scenarios/auth/byok-azure/typescript/src/index.ts +++ b/test/scenarios/auth/byok-azure/typescript/src/index.ts @@ -40,7 +40,7 @@ async function main() { console.log(response.data.content); } - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/auth/byok-ollama/go/main.go b/test/scenarios/auth/byok-ollama/go/main.go index 191d2eab..b8b34c5b 100644 --- a/test/scenarios/auth/byok-ollama/go/main.go +++ b/test/scenarios/auth/byok-ollama/go/main.go @@ -45,7 +45,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "What is the capital of France?", diff --git a/test/scenarios/auth/byok-ollama/python/main.py b/test/scenarios/auth/byok-ollama/python/main.py index 0f9df7f5..b86c76ba 100644 --- a/test/scenarios/auth/byok-ollama/python/main.py +++ b/test/scenarios/auth/byok-ollama/python/main.py @@ -38,7 +38,7 @@ async def main(): if response: print(response.data.content) - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/auth/byok-ollama/typescript/src/index.ts b/test/scenarios/auth/byok-ollama/typescript/src/index.ts index 3ba9da89..936d118a 100644 --- a/test/scenarios/auth/byok-ollama/typescript/src/index.ts +++ b/test/scenarios/auth/byok-ollama/typescript/src/index.ts @@ -31,7 +31,7 @@ async function main() { console.log(response.data.content); } - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/auth/byok-openai/go/main.go b/test/scenarios/auth/byok-openai/go/main.go index bd418ab7..fc05c71b 100644 --- a/test/scenarios/auth/byok-openai/go/main.go +++ b/test/scenarios/auth/byok-openai/go/main.go @@ -44,7 +44,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "What is the capital of France?", diff --git a/test/scenarios/auth/byok-openai/python/main.py b/test/scenarios/auth/byok-openai/python/main.py index 651a92cd..b501bb10 100644 --- a/test/scenarios/auth/byok-openai/python/main.py +++ b/test/scenarios/auth/byok-openai/python/main.py @@ -35,7 +35,7 @@ async def main(): if response: print(response.data.content) - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/auth/byok-openai/typescript/src/index.ts b/test/scenarios/auth/byok-openai/typescript/src/index.ts index 1d2d0aaf..41eda577 100644 --- a/test/scenarios/auth/byok-openai/typescript/src/index.ts +++ b/test/scenarios/auth/byok-openai/typescript/src/index.ts @@ -32,7 +32,7 @@ async function main() { console.log(response.data.content); } - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/auth/gh-app/go/main.go b/test/scenarios/auth/gh-app/go/main.go index 4aaad3b4..d84d030c 100644 --- a/test/scenarios/auth/gh-app/go/main.go +++ b/test/scenarios/auth/gh-app/go/main.go @@ -177,7 +177,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "What is the capital of France?", diff --git a/test/scenarios/auth/gh-app/python/main.py b/test/scenarios/auth/gh-app/python/main.py index 4568c82b..4886fe07 100644 --- a/test/scenarios/auth/gh-app/python/main.py +++ b/test/scenarios/auth/gh-app/python/main.py @@ -88,7 +88,7 @@ async def main(): response = await session.send_and_wait({"prompt": "What is the capital of France?"}) if response: print(response.data.content) - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/auth/gh-app/typescript/src/index.ts b/test/scenarios/auth/gh-app/typescript/src/index.ts index 1c9cabde..a5b8f28e 100644 --- a/test/scenarios/auth/gh-app/typescript/src/index.ts +++ b/test/scenarios/auth/gh-app/typescript/src/index.ts @@ -121,7 +121,7 @@ async function main() { }); if (response) console.log(response.data.content); - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/bundling/app-backend-to-server/go/main.go b/test/scenarios/bundling/app-backend-to-server/go/main.go index afc8858f..df2be62b 100644 --- a/test/scenarios/bundling/app-backend-to-server/go/main.go +++ b/test/scenarios/bundling/app-backend-to-server/go/main.go @@ -70,7 +70,7 @@ func chatHandler(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusInternalServerError, chatResponse{Error: err.Error()}) return } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: req.Prompt, diff --git a/test/scenarios/bundling/app-backend-to-server/python/main.py b/test/scenarios/bundling/app-backend-to-server/python/main.py index 218505f4..29563149 100644 --- a/test/scenarios/bundling/app-backend-to-server/python/main.py +++ b/test/scenarios/bundling/app-backend-to-server/python/main.py @@ -20,7 +20,7 @@ async def ask_copilot(prompt: str) -> str: response = await session.send_and_wait({"prompt": prompt}) - await session.destroy() + await session.disconnect() if response: return response.data.content diff --git a/test/scenarios/bundling/app-backend-to-server/typescript/src/index.ts b/test/scenarios/bundling/app-backend-to-server/typescript/src/index.ts index 3394c0d3..7ab734d1 100644 --- a/test/scenarios/bundling/app-backend-to-server/typescript/src/index.ts +++ b/test/scenarios/bundling/app-backend-to-server/typescript/src/index.ts @@ -21,7 +21,7 @@ app.post("/chat", async (req, res) => { const response = await session.sendAndWait({ prompt }); - await session.destroy(); + await session.disconnect(); if (response?.data.content) { res.json({ response: response.data.content }); diff --git a/test/scenarios/bundling/app-direct-server/go/main.go b/test/scenarios/bundling/app-direct-server/go/main.go index 9a0b1be4..8be7dd60 100644 --- a/test/scenarios/bundling/app-direct-server/go/main.go +++ b/test/scenarios/bundling/app-direct-server/go/main.go @@ -31,7 +31,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "What is the capital of France?", diff --git a/test/scenarios/bundling/app-direct-server/python/main.py b/test/scenarios/bundling/app-direct-server/python/main.py index 05aaa927..c407d4fe 100644 --- a/test/scenarios/bundling/app-direct-server/python/main.py +++ b/test/scenarios/bundling/app-direct-server/python/main.py @@ -18,7 +18,7 @@ async def main(): if response: print(response.data.content) - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/bundling/app-direct-server/typescript/src/index.ts b/test/scenarios/bundling/app-direct-server/typescript/src/index.ts index 139e47a8..29a19dd1 100644 --- a/test/scenarios/bundling/app-direct-server/typescript/src/index.ts +++ b/test/scenarios/bundling/app-direct-server/typescript/src/index.ts @@ -19,7 +19,7 @@ async function main() { process.exit(1); } - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/bundling/container-proxy/go/main.go b/test/scenarios/bundling/container-proxy/go/main.go index 9a0b1be4..8be7dd60 100644 --- a/test/scenarios/bundling/container-proxy/go/main.go +++ b/test/scenarios/bundling/container-proxy/go/main.go @@ -31,7 +31,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "What is the capital of France?", diff --git a/test/scenarios/bundling/container-proxy/python/main.py b/test/scenarios/bundling/container-proxy/python/main.py index 05aaa927..c407d4fe 100644 --- a/test/scenarios/bundling/container-proxy/python/main.py +++ b/test/scenarios/bundling/container-proxy/python/main.py @@ -18,7 +18,7 @@ async def main(): if response: print(response.data.content) - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/bundling/container-proxy/typescript/src/index.ts b/test/scenarios/bundling/container-proxy/typescript/src/index.ts index 139e47a8..29a19dd1 100644 --- a/test/scenarios/bundling/container-proxy/typescript/src/index.ts +++ b/test/scenarios/bundling/container-proxy/typescript/src/index.ts @@ -19,7 +19,7 @@ async function main() { process.exit(1); } - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/bundling/fully-bundled/go/main.go b/test/scenarios/bundling/fully-bundled/go/main.go index e548a08e..b8902fd9 100644 --- a/test/scenarios/bundling/fully-bundled/go/main.go +++ b/test/scenarios/bundling/fully-bundled/go/main.go @@ -27,7 +27,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "What is the capital of France?", diff --git a/test/scenarios/bundling/fully-bundled/python/main.py b/test/scenarios/bundling/fully-bundled/python/main.py index 138bb564..d1441361 100644 --- a/test/scenarios/bundling/fully-bundled/python/main.py +++ b/test/scenarios/bundling/fully-bundled/python/main.py @@ -19,7 +19,7 @@ async def main(): if response: print(response.data.content) - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/bundling/fully-bundled/typescript/src/index.ts b/test/scenarios/bundling/fully-bundled/typescript/src/index.ts index 989a0b9a..bee246f6 100644 --- a/test/scenarios/bundling/fully-bundled/typescript/src/index.ts +++ b/test/scenarios/bundling/fully-bundled/typescript/src/index.ts @@ -17,7 +17,7 @@ async function main() { console.log(response.data.content); } - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/callbacks/hooks/go/main.go b/test/scenarios/callbacks/hooks/go/main.go index c084c3a7..44e6e024 100644 --- a/test/scenarios/callbacks/hooks/go/main.go +++ b/test/scenarios/callbacks/hooks/go/main.go @@ -67,7 +67,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "List the files in the current directory using the glob tool with pattern '*.md'.", diff --git a/test/scenarios/callbacks/hooks/python/main.py b/test/scenarios/callbacks/hooks/python/main.py index a00c18af..8df61b9d 100644 --- a/test/scenarios/callbacks/hooks/python/main.py +++ b/test/scenarios/callbacks/hooks/python/main.py @@ -70,7 +70,7 @@ async def main(): if response: print(response.data.content) - await session.destroy() + await session.disconnect() print("\n--- Hook execution log ---") for entry in hook_log: diff --git a/test/scenarios/callbacks/hooks/typescript/src/index.ts b/test/scenarios/callbacks/hooks/typescript/src/index.ts index 52708d8f..2a5cde58 100644 --- a/test/scenarios/callbacks/hooks/typescript/src/index.ts +++ b/test/scenarios/callbacks/hooks/typescript/src/index.ts @@ -44,7 +44,7 @@ async function main() { console.log(response.data.content); } - await session.destroy(); + await session.disconnect(); console.log("\n--- Hook execution log ---"); for (const entry of hookLog) { diff --git a/test/scenarios/callbacks/permissions/go/main.go b/test/scenarios/callbacks/permissions/go/main.go index 1ac49c04..f70bdfbf 100644 --- a/test/scenarios/callbacks/permissions/go/main.go +++ b/test/scenarios/callbacks/permissions/go/main.go @@ -44,7 +44,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "List the files in the current directory using glob with pattern '*.md'.", diff --git a/test/scenarios/callbacks/permissions/python/main.py b/test/scenarios/callbacks/permissions/python/main.py index 2da5133f..9674da91 100644 --- a/test/scenarios/callbacks/permissions/python/main.py +++ b/test/scenarios/callbacks/permissions/python/main.py @@ -39,7 +39,7 @@ async def main(): if response: print(response.data.content) - await session.destroy() + await session.disconnect() print("\n--- Permission request log ---") for entry in permission_log: diff --git a/test/scenarios/callbacks/permissions/typescript/src/index.ts b/test/scenarios/callbacks/permissions/typescript/src/index.ts index a7e452cc..6a163bc2 100644 --- a/test/scenarios/callbacks/permissions/typescript/src/index.ts +++ b/test/scenarios/callbacks/permissions/typescript/src/index.ts @@ -31,7 +31,7 @@ async function main() { console.log(response.data.content); } - await session.destroy(); + await session.disconnect(); console.log("\n--- Permission request log ---"); for (const entry of permissionLog) { diff --git a/test/scenarios/callbacks/user-input/go/main.go b/test/scenarios/callbacks/user-input/go/main.go index 91d0c86e..50eb65a2 100644 --- a/test/scenarios/callbacks/user-input/go/main.go +++ b/test/scenarios/callbacks/user-input/go/main.go @@ -46,7 +46,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "I want to learn about a city. Use the ask_user tool to ask me " + diff --git a/test/scenarios/callbacks/user-input/python/main.py b/test/scenarios/callbacks/user-input/python/main.py index fb36eda5..dc8d9fa9 100644 --- a/test/scenarios/callbacks/user-input/python/main.py +++ b/test/scenarios/callbacks/user-input/python/main.py @@ -47,7 +47,7 @@ async def main(): if response: print(response.data.content) - await session.destroy() + await session.disconnect() print("\n--- User input log ---") for entry in input_log: diff --git a/test/scenarios/callbacks/user-input/typescript/src/index.ts b/test/scenarios/callbacks/user-input/typescript/src/index.ts index 4791fcf1..5964ce6c 100644 --- a/test/scenarios/callbacks/user-input/typescript/src/index.ts +++ b/test/scenarios/callbacks/user-input/typescript/src/index.ts @@ -29,7 +29,7 @@ async function main() { console.log(response.data.content); } - await session.destroy(); + await session.disconnect(); console.log("\n--- User input log ---"); for (const entry of inputLog) { diff --git a/test/scenarios/modes/default/go/main.go b/test/scenarios/modes/default/go/main.go index dfae2517..dd2b45d3 100644 --- a/test/scenarios/modes/default/go/main.go +++ b/test/scenarios/modes/default/go/main.go @@ -26,7 +26,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "Use the grep tool to search for the word 'SDK' in README.md and show the matching lines.", diff --git a/test/scenarios/modes/default/python/main.py b/test/scenarios/modes/default/python/main.py index 0abc6b70..dadc0e7b 100644 --- a/test/scenarios/modes/default/python/main.py +++ b/test/scenarios/modes/default/python/main.py @@ -20,7 +20,7 @@ async def main(): print("Default mode test complete") - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/modes/default/typescript/src/index.ts b/test/scenarios/modes/default/typescript/src/index.ts index e10cb6cb..89aab359 100644 --- a/test/scenarios/modes/default/typescript/src/index.ts +++ b/test/scenarios/modes/default/typescript/src/index.ts @@ -21,7 +21,7 @@ async function main() { console.log("Default mode test complete"); - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/modes/minimal/go/main.go b/test/scenarios/modes/minimal/go/main.go index c39c24f6..c3624b11 100644 --- a/test/scenarios/modes/minimal/go/main.go +++ b/test/scenarios/modes/minimal/go/main.go @@ -31,7 +31,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "Use the grep tool to search for 'SDK' in README.md.", diff --git a/test/scenarios/modes/minimal/python/main.py b/test/scenarios/modes/minimal/python/main.py index 74a98ba0..0b243caf 100644 --- a/test/scenarios/modes/minimal/python/main.py +++ b/test/scenarios/modes/minimal/python/main.py @@ -25,7 +25,7 @@ async def main(): print("Minimal mode test complete") - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/modes/minimal/typescript/src/index.ts b/test/scenarios/modes/minimal/typescript/src/index.ts index 091595be..f20e476d 100644 --- a/test/scenarios/modes/minimal/typescript/src/index.ts +++ b/test/scenarios/modes/minimal/typescript/src/index.ts @@ -26,7 +26,7 @@ async function main() { console.log("Minimal mode test complete"); - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/prompts/attachments/go/main.go b/test/scenarios/prompts/attachments/go/main.go index 4b248bf9..95eb2b4d 100644 --- a/test/scenarios/prompts/attachments/go/main.go +++ b/test/scenarios/prompts/attachments/go/main.go @@ -34,7 +34,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() exe, err := os.Executable() if err != nil { diff --git a/test/scenarios/prompts/attachments/python/main.py b/test/scenarios/prompts/attachments/python/main.py index acf9c7af..c7e21e8b 100644 --- a/test/scenarios/prompts/attachments/python/main.py +++ b/test/scenarios/prompts/attachments/python/main.py @@ -33,7 +33,7 @@ async def main(): if response: print(response.data.content) - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/prompts/attachments/typescript/src/index.ts b/test/scenarios/prompts/attachments/typescript/src/index.ts index 72e601ca..100f7e17 100644 --- a/test/scenarios/prompts/attachments/typescript/src/index.ts +++ b/test/scenarios/prompts/attachments/typescript/src/index.ts @@ -31,7 +31,7 @@ async function main() { console.log(response.data.content); } - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/prompts/reasoning-effort/go/main.go b/test/scenarios/prompts/reasoning-effort/go/main.go index 43c5eb74..ccb4e528 100644 --- a/test/scenarios/prompts/reasoning-effort/go/main.go +++ b/test/scenarios/prompts/reasoning-effort/go/main.go @@ -32,7 +32,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "What is the capital of France?", diff --git a/test/scenarios/prompts/reasoning-effort/python/main.py b/test/scenarios/prompts/reasoning-effort/python/main.py index 74444e7b..b38452a8 100644 --- a/test/scenarios/prompts/reasoning-effort/python/main.py +++ b/test/scenarios/prompts/reasoning-effort/python/main.py @@ -28,7 +28,7 @@ async def main(): print("Reasoning effort: low") print(f"Response: {response.data.content}") - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts b/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts index fd2091ef..e569fd70 100644 --- a/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts +++ b/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts @@ -27,7 +27,7 @@ async function main() { console.log(`Response: ${response.data.content}`); } - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/prompts/system-message/go/main.go b/test/scenarios/prompts/system-message/go/main.go index aeef7613..074c9994 100644 --- a/test/scenarios/prompts/system-message/go/main.go +++ b/test/scenarios/prompts/system-message/go/main.go @@ -33,7 +33,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "What is the capital of France?", diff --git a/test/scenarios/prompts/system-message/python/main.py b/test/scenarios/prompts/system-message/python/main.py index a3bfccdc..5e396c8c 100644 --- a/test/scenarios/prompts/system-message/python/main.py +++ b/test/scenarios/prompts/system-message/python/main.py @@ -27,7 +27,7 @@ async def main(): if response: print(response.data.content) - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/prompts/system-message/typescript/src/index.ts b/test/scenarios/prompts/system-message/typescript/src/index.ts index dc518069..e0eb0aab 100644 --- a/test/scenarios/prompts/system-message/typescript/src/index.ts +++ b/test/scenarios/prompts/system-message/typescript/src/index.ts @@ -23,7 +23,7 @@ async function main() { console.log(response.data.content); } - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/sessions/concurrent-sessions/go/main.go b/test/scenarios/sessions/concurrent-sessions/go/main.go index 02b3f03a..ced91553 100644 --- a/test/scenarios/sessions/concurrent-sessions/go/main.go +++ b/test/scenarios/sessions/concurrent-sessions/go/main.go @@ -35,7 +35,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session1.Destroy() + defer session1.Disconnect() session2, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", @@ -48,7 +48,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session2.Destroy() + defer session2.Disconnect() type result struct { label string diff --git a/test/scenarios/sessions/concurrent-sessions/python/main.py b/test/scenarios/sessions/concurrent-sessions/python/main.py index 171a202e..ebca8990 100644 --- a/test/scenarios/sessions/concurrent-sessions/python/main.py +++ b/test/scenarios/sessions/concurrent-sessions/python/main.py @@ -44,7 +44,7 @@ async def main(): if response2: print("Session 2 (robot):", response2.data.content) - await asyncio.gather(session1.destroy(), session2.destroy()) + await asyncio.gather(session1.disconnect(), session2.disconnect()) finally: await client.stop() diff --git a/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts b/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts index 80772886..89543d28 100644 --- a/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts +++ b/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts @@ -35,7 +35,7 @@ async function main() { console.log("Session 2 (robot):", response2.data.content); } - await Promise.all([session1.destroy(), session2.destroy()]); + await Promise.all([session1.disconnect(), session2.disconnect()]); } finally { await client.stop(); process.exit(0); diff --git a/test/scenarios/sessions/infinite-sessions/go/main.go b/test/scenarios/sessions/infinite-sessions/go/main.go index 38090660..540f8f6b 100644 --- a/test/scenarios/sessions/infinite-sessions/go/main.go +++ b/test/scenarios/sessions/infinite-sessions/go/main.go @@ -39,7 +39,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() prompts := []string{ "What is the capital of France?", diff --git a/test/scenarios/sessions/infinite-sessions/python/main.py b/test/scenarios/sessions/infinite-sessions/python/main.py index fe39a711..23749d06 100644 --- a/test/scenarios/sessions/infinite-sessions/python/main.py +++ b/test/scenarios/sessions/infinite-sessions/python/main.py @@ -38,7 +38,7 @@ async def main(): print("Infinite sessions test complete — all messages processed successfully") - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts b/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts index a3b3de61..9de7b34f 100644 --- a/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts +++ b/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts @@ -37,7 +37,7 @@ async function main() { console.log("Infinite sessions test complete — all messages processed successfully"); - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/sessions/multi-user-short-lived/README.md b/test/scenarios/sessions/multi-user-short-lived/README.md index 6596fa7b..17e7e127 100644 --- a/test/scenarios/sessions/multi-user-short-lived/README.md +++ b/test/scenarios/sessions/multi-user-short-lived/README.md @@ -17,7 +17,7 @@ Demonstrates a **stateless backend pattern** where multiple users interact with │(new) │ │(new)│ │(new) │ └──────┘ └─────┘ └──────┘ -Each request → new session → destroy after response +Each request → new session → disconnect after response Virtual FS per user (in-memory, not shared across users) ``` diff --git a/test/scenarios/sessions/session-resume/go/main.go b/test/scenarios/sessions/session-resume/go/main.go index 6ec4bb02..2ba0b24b 100644 --- a/test/scenarios/sessions/session-resume/go/main.go +++ b/test/scenarios/sessions/session-resume/go/main.go @@ -38,7 +38,7 @@ func main() { log.Fatal(err) } - // 3. Get the session ID (don't destroy — resume needs the session to persist) + // 3. Get the session ID (don't disconnect — resume needs the session to persist) sessionID := session.SessionID // 4. Resume the session with the same ID @@ -49,7 +49,7 @@ func main() { log.Fatal(err) } fmt.Println("Session resumed") - defer resumed.Destroy() + defer resumed.Disconnect() // 5. Ask for the secret word response, err := resumed.SendAndWait(ctx, copilot.MessageOptions{ diff --git a/test/scenarios/sessions/session-resume/python/main.py b/test/scenarios/sessions/session-resume/python/main.py index b65370b9..7eb5e0ca 100644 --- a/test/scenarios/sessions/session-resume/python/main.py +++ b/test/scenarios/sessions/session-resume/python/main.py @@ -23,7 +23,7 @@ async def main(): {"prompt": "Remember this: the secret word is PINEAPPLE."} ) - # 3. Get the session ID (don't destroy — resume needs the session to persist) + # 3. Get the session ID (don't disconnect — resume needs the session to persist) session_id = session.session_id # 4. Resume the session with the same ID @@ -38,7 +38,7 @@ async def main(): if response: print(response.data.content) - await resumed.destroy() + await resumed.disconnect() finally: await client.stop() diff --git a/test/scenarios/sessions/session-resume/typescript/src/index.ts b/test/scenarios/sessions/session-resume/typescript/src/index.ts index 7d08f40e..9e0a1685 100644 --- a/test/scenarios/sessions/session-resume/typescript/src/index.ts +++ b/test/scenarios/sessions/session-resume/typescript/src/index.ts @@ -18,7 +18,7 @@ async function main() { prompt: "Remember this: the secret word is PINEAPPLE.", }); - // 3. Get the session ID (don't destroy — resume needs the session to persist) + // 3. Get the session ID (don't disconnect — resume needs the session to persist) const sessionId = session.sessionId; // 4. Resume the session with the same ID @@ -34,7 +34,7 @@ async function main() { console.log(response.data.content); } - await resumed.destroy(); + await resumed.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/sessions/streaming/go/main.go b/test/scenarios/sessions/streaming/go/main.go index 0be9ae03..6243a166 100644 --- a/test/scenarios/sessions/streaming/go/main.go +++ b/test/scenarios/sessions/streaming/go/main.go @@ -27,7 +27,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() chunkCount := 0 session.On(func(event copilot.SessionEvent) { diff --git a/test/scenarios/sessions/streaming/python/main.py b/test/scenarios/sessions/streaming/python/main.py index 2bbc94e7..94569de1 100644 --- a/test/scenarios/sessions/streaming/python/main.py +++ b/test/scenarios/sessions/streaming/python/main.py @@ -34,7 +34,7 @@ def on_event(event): print(response.data.content) print(f"\nStreaming chunks received: {chunk_count}") - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/sessions/streaming/typescript/src/index.ts b/test/scenarios/sessions/streaming/typescript/src/index.ts index fb0a23be..f70dccce 100644 --- a/test/scenarios/sessions/streaming/typescript/src/index.ts +++ b/test/scenarios/sessions/streaming/typescript/src/index.ts @@ -26,7 +26,7 @@ async function main() { } console.log(`\nStreaming chunks received: ${chunkCount}`); - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/tools/custom-agents/go/main.go b/test/scenarios/tools/custom-agents/go/main.go index 1ce90d47..f2add822 100644 --- a/test/scenarios/tools/custom-agents/go/main.go +++ b/test/scenarios/tools/custom-agents/go/main.go @@ -35,7 +35,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "What custom agents are available? Describe the researcher agent and its capabilities.", diff --git a/test/scenarios/tools/custom-agents/python/main.py b/test/scenarios/tools/custom-agents/python/main.py index d4e41671..0b5f073d 100644 --- a/test/scenarios/tools/custom-agents/python/main.py +++ b/test/scenarios/tools/custom-agents/python/main.py @@ -32,7 +32,7 @@ async def main(): if response: print(response.data.content) - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/tools/custom-agents/typescript/src/index.ts b/test/scenarios/tools/custom-agents/typescript/src/index.ts index b098bffa..f6e16325 100644 --- a/test/scenarios/tools/custom-agents/typescript/src/index.ts +++ b/test/scenarios/tools/custom-agents/typescript/src/index.ts @@ -28,7 +28,7 @@ async function main() { console.log(response.data.content); } - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/tools/mcp-servers/go/main.go b/test/scenarios/tools/mcp-servers/go/main.go index 70831caf..a6e2e9c1 100644 --- a/test/scenarios/tools/mcp-servers/go/main.go +++ b/test/scenarios/tools/mcp-servers/go/main.go @@ -53,7 +53,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "What is the capital of France?", diff --git a/test/scenarios/tools/mcp-servers/python/main.py b/test/scenarios/tools/mcp-servers/python/main.py index 81d2e39b..f092fb9a 100644 --- a/test/scenarios/tools/mcp-servers/python/main.py +++ b/test/scenarios/tools/mcp-servers/python/main.py @@ -47,7 +47,7 @@ async def main(): else: print("\nNo MCP servers configured (set MCP_SERVER_CMD to test with a real server)") - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/tools/mcp-servers/typescript/src/index.ts b/test/scenarios/tools/mcp-servers/typescript/src/index.ts index 41afa583..1e8c1146 100644 --- a/test/scenarios/tools/mcp-servers/typescript/src/index.ts +++ b/test/scenarios/tools/mcp-servers/typescript/src/index.ts @@ -43,7 +43,7 @@ async function main() { console.log("\nNo MCP servers configured (set MCP_SERVER_CMD to test with a real server)"); } - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/tools/no-tools/go/main.go b/test/scenarios/tools/no-tools/go/main.go index d453f0df..62af3bce 100644 --- a/test/scenarios/tools/no-tools/go/main.go +++ b/test/scenarios/tools/no-tools/go/main.go @@ -36,7 +36,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "Use the bash tool to run 'echo hello'.", diff --git a/test/scenarios/tools/no-tools/python/main.py b/test/scenarios/tools/no-tools/python/main.py index d857183c..a3824bab 100644 --- a/test/scenarios/tools/no-tools/python/main.py +++ b/test/scenarios/tools/no-tools/python/main.py @@ -30,7 +30,7 @@ async def main(): if response: print(response.data.content) - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/tools/no-tools/typescript/src/index.ts b/test/scenarios/tools/no-tools/typescript/src/index.ts index dea9c4f1..487b4762 100644 --- a/test/scenarios/tools/no-tools/typescript/src/index.ts +++ b/test/scenarios/tools/no-tools/typescript/src/index.ts @@ -26,7 +26,7 @@ async function main() { console.log(response.data.content); } - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/tools/skills/go/main.go b/test/scenarios/tools/skills/go/main.go index e322dda6..5652de32 100644 --- a/test/scenarios/tools/skills/go/main.go +++ b/test/scenarios/tools/skills/go/main.go @@ -40,7 +40,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "Use the greeting skill to greet someone named Alice.", diff --git a/test/scenarios/tools/skills/python/main.py b/test/scenarios/tools/skills/python/main.py index 5adb74b7..3e06650b 100644 --- a/test/scenarios/tools/skills/python/main.py +++ b/test/scenarios/tools/skills/python/main.py @@ -34,7 +34,7 @@ async def main(): print("\nSkill directories configured successfully") - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/tools/skills/typescript/src/index.ts b/test/scenarios/tools/skills/typescript/src/index.ts index fa4b3372..de7f1356 100644 --- a/test/scenarios/tools/skills/typescript/src/index.ts +++ b/test/scenarios/tools/skills/typescript/src/index.ts @@ -32,7 +32,7 @@ async function main() { console.log("\nSkill directories configured successfully"); - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/tools/tool-filtering/go/main.go b/test/scenarios/tools/tool-filtering/go/main.go index a774fb3e..851ca311 100644 --- a/test/scenarios/tools/tool-filtering/go/main.go +++ b/test/scenarios/tools/tool-filtering/go/main.go @@ -33,7 +33,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "What tools do you have available? List each one by name.", diff --git a/test/scenarios/tools/tool-filtering/python/main.py b/test/scenarios/tools/tool-filtering/python/main.py index 174be620..1fdfacc7 100644 --- a/test/scenarios/tools/tool-filtering/python/main.py +++ b/test/scenarios/tools/tool-filtering/python/main.py @@ -27,7 +27,7 @@ async def main(): if response: print(response.data.content) - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/tools/tool-filtering/typescript/src/index.ts b/test/scenarios/tools/tool-filtering/typescript/src/index.ts index 40cc9112..9976e38f 100644 --- a/test/scenarios/tools/tool-filtering/typescript/src/index.ts +++ b/test/scenarios/tools/tool-filtering/typescript/src/index.ts @@ -24,7 +24,7 @@ async function main() { console.log(response.data.content); } - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/tools/virtual-filesystem/go/main.go b/test/scenarios/tools/virtual-filesystem/go/main.go index 29b1eef4..39e3d910 100644 --- a/test/scenarios/tools/virtual-filesystem/go/main.go +++ b/test/scenarios/tools/virtual-filesystem/go/main.go @@ -100,7 +100,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "Create a file called plan.md with a brief 3-item project plan " + diff --git a/test/scenarios/tools/virtual-filesystem/python/main.py b/test/scenarios/tools/virtual-filesystem/python/main.py index b150c1a2..9a51e7ef 100644 --- a/test/scenarios/tools/virtual-filesystem/python/main.py +++ b/test/scenarios/tools/virtual-filesystem/python/main.py @@ -80,7 +80,7 @@ async def main(): print(f"\n[{path}]") print(content) - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts b/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts index 0a6f0ffd..4f7dadfd 100644 --- a/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts +++ b/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts @@ -74,7 +74,7 @@ async function main() { console.log(content); } - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/transport/reconnect/README.md b/test/scenarios/transport/reconnect/README.md index 4ae3c22d..c2ed0d2f 100644 --- a/test/scenarios/transport/reconnect/README.md +++ b/test/scenarios/transport/reconnect/README.md @@ -7,8 +7,8 @@ Tests that a **pre-running** `copilot` TCP server correctly handles **multiple s │ Your App │ ─────────────────▶ │ Copilot CLI │ │ (SDK) │ ◀───────────────── │ (TCP server) │ └─────────────┘ └──────────────┘ - Session 1: create → send → destroy - Session 2: create → send → destroy + Session 1: create → send → disconnect + Session 2: create → send → disconnect ``` ## What This Tests diff --git a/test/scenarios/transport/reconnect/csharp/Program.cs b/test/scenarios/transport/reconnect/csharp/Program.cs index a93ed8a7..80dc482d 100644 --- a/test/scenarios/transport/reconnect/csharp/Program.cs +++ b/test/scenarios/transport/reconnect/csharp/Program.cs @@ -28,7 +28,7 @@ Console.Error.WriteLine("No response content received for session 1"); Environment.Exit(1); } - Console.WriteLine("Session 1 destroyed\n"); + Console.WriteLine("Session 1 disconnected\n"); // Second session — tests that the server accepts new sessions Console.WriteLine("--- Session 2 ---"); @@ -51,7 +51,7 @@ Console.Error.WriteLine("No response content received for session 2"); Environment.Exit(1); } - Console.WriteLine("Session 2 destroyed"); + Console.WriteLine("Session 2 disconnected"); Console.WriteLine("\nReconnect test passed — both sessions completed successfully"); } diff --git a/test/scenarios/transport/reconnect/go/main.go b/test/scenarios/transport/reconnect/go/main.go index 27f6c159..493e9d25 100644 --- a/test/scenarios/transport/reconnect/go/main.go +++ b/test/scenarios/transport/reconnect/go/main.go @@ -43,8 +43,8 @@ func main() { log.Fatal("No response content received for session 1") } - session1.Destroy() - fmt.Println("Session 1 destroyed") + session1.Disconnect() + fmt.Println("Session 1 disconnected") fmt.Println() // Session 2 — tests that the server accepts new sessions @@ -69,8 +69,8 @@ func main() { log.Fatal("No response content received for session 2") } - session2.Destroy() - fmt.Println("Session 2 destroyed") + session2.Disconnect() + fmt.Println("Session 2 disconnected") fmt.Println("\nReconnect test passed — both sessions completed successfully") } diff --git a/test/scenarios/transport/reconnect/python/main.py b/test/scenarios/transport/reconnect/python/main.py index e8aecea5..1b82b109 100644 --- a/test/scenarios/transport/reconnect/python/main.py +++ b/test/scenarios/transport/reconnect/python/main.py @@ -24,8 +24,8 @@ async def main(): print("No response content received for session 1", file=sys.stderr) sys.exit(1) - await session1.destroy() - print("Session 1 destroyed\n") + await session1.disconnect() + print("Session 1 disconnected\n") # Second session — tests that the server accepts new sessions print("--- Session 2 ---") @@ -41,8 +41,8 @@ async def main(): print("No response content received for session 2", file=sys.stderr) sys.exit(1) - await session2.destroy() - print("Session 2 destroyed") + await session2.disconnect() + print("Session 2 disconnected") print("\nReconnect test passed — both sessions completed successfully") finally: diff --git a/test/scenarios/transport/reconnect/typescript/src/index.ts b/test/scenarios/transport/reconnect/typescript/src/index.ts index 57bac483..ca28df94 100644 --- a/test/scenarios/transport/reconnect/typescript/src/index.ts +++ b/test/scenarios/transport/reconnect/typescript/src/index.ts @@ -21,8 +21,8 @@ async function main() { process.exit(1); } - await session1.destroy(); - console.log("Session 1 destroyed\n"); + await session1.disconnect(); + console.log("Session 1 disconnected\n"); // Second session — tests that the server accepts new sessions console.log("--- Session 2 ---"); @@ -39,8 +39,8 @@ async function main() { process.exit(1); } - await session2.destroy(); - console.log("Session 2 destroyed"); + await session2.disconnect(); + console.log("Session 2 disconnected"); console.log("\nReconnect test passed — both sessions completed successfully"); } finally { diff --git a/test/scenarios/transport/stdio/go/main.go b/test/scenarios/transport/stdio/go/main.go index e548a08e..b8902fd9 100644 --- a/test/scenarios/transport/stdio/go/main.go +++ b/test/scenarios/transport/stdio/go/main.go @@ -27,7 +27,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "What is the capital of France?", diff --git a/test/scenarios/transport/stdio/python/main.py b/test/scenarios/transport/stdio/python/main.py index 138bb564..d1441361 100644 --- a/test/scenarios/transport/stdio/python/main.py +++ b/test/scenarios/transport/stdio/python/main.py @@ -19,7 +19,7 @@ async def main(): if response: print(response.data.content) - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/transport/stdio/typescript/src/index.ts b/test/scenarios/transport/stdio/typescript/src/index.ts index 989a0b9a..bee246f6 100644 --- a/test/scenarios/transport/stdio/typescript/src/index.ts +++ b/test/scenarios/transport/stdio/typescript/src/index.ts @@ -17,7 +17,7 @@ async function main() { console.log(response.data.content); } - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } diff --git a/test/scenarios/transport/tcp/go/main.go b/test/scenarios/transport/tcp/go/main.go index 9a0b1be4..8be7dd60 100644 --- a/test/scenarios/transport/tcp/go/main.go +++ b/test/scenarios/transport/tcp/go/main.go @@ -31,7 +31,7 @@ func main() { if err != nil { log.Fatal(err) } - defer session.Destroy() + defer session.Disconnect() response, err := session.SendAndWait(ctx, copilot.MessageOptions{ Prompt: "What is the capital of France?", diff --git a/test/scenarios/transport/tcp/python/main.py b/test/scenarios/transport/tcp/python/main.py index 05aaa927..c407d4fe 100644 --- a/test/scenarios/transport/tcp/python/main.py +++ b/test/scenarios/transport/tcp/python/main.py @@ -18,7 +18,7 @@ async def main(): if response: print(response.data.content) - await session.destroy() + await session.disconnect() finally: await client.stop() diff --git a/test/scenarios/transport/tcp/typescript/src/index.ts b/test/scenarios/transport/tcp/typescript/src/index.ts index 139e47a8..29a19dd1 100644 --- a/test/scenarios/transport/tcp/typescript/src/index.ts +++ b/test/scenarios/transport/tcp/typescript/src/index.ts @@ -19,7 +19,7 @@ async function main() { process.exit(1); } - await session.destroy(); + await session.disconnect(); } finally { await client.stop(); } From fbc948b534524ee70306a57e9bfbc21a352d19ba Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Wed, 4 Mar 2026 20:21:58 -0800 Subject: [PATCH 4/5] fix: rename snapshot to match updated test name The hooks_extended test 'should invoke onSessionEnd hook when session is destroyed' was renamed to '...disconnected', but the snapshot YAML file wasn't renamed to match, causing CI to fail with 'No cached response'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ld_invoke_onsessionend_hook_when_session_is_disconnected.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/snapshots/hooks_extended/{should_invoke_onsessionend_hook_when_session_is_destroyed.yaml => should_invoke_onsessionend_hook_when_session_is_disconnected.yaml} (100%) diff --git a/test/snapshots/hooks_extended/should_invoke_onsessionend_hook_when_session_is_destroyed.yaml b/test/snapshots/hooks_extended/should_invoke_onsessionend_hook_when_session_is_disconnected.yaml similarity index 100% rename from test/snapshots/hooks_extended/should_invoke_onsessionend_hook_when_session_is_destroyed.yaml rename to test/snapshots/hooks_extended/should_invoke_onsessionend_hook_when_session_is_disconnected.yaml From 7c3c0e6950a90423d9c4d403b6038533fee54112 Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Thu, 5 Mar 2026 13:52:39 -0800 Subject: [PATCH 5/5] dotnet: remove DisconnectAsync, keep only DisposeAsync Address review feedback from SteveSandersonMS: for .NET, the standard IAsyncDisposable pattern (DisposeAsync) is sufficient on its own without a duplicate DisconnectAsync method. Moves the disconnect implementation directly into DisposeAsync and removes the separate DisconnectAsync method. Updates all references in Client.cs and README.md accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/README.md | 12 ++++------- dotnet/src/Client.cs | 6 +++--- dotnet/src/Session.cs | 48 ++++++++++--------------------------------- 3 files changed, 18 insertions(+), 48 deletions(-) diff --git a/dotnet/README.md b/dotnet/README.md index ba942796..bdb3e8da 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -217,22 +217,18 @@ 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` -Calls `DisconnectAsync()`. Enables the `await using` pattern for automatic cleanup: +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 disconnected when leaving scope +// session is automatically disposed when leaving scope -// Alternative: explicit disconnect +// Alternative: explicit dispose var session2 = await client.CreateSessionAsync(config); -await session2.DisconnectAsync(); +await session2.DisposeAsync(); ``` --- diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 399c9891..a340cd63 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -243,11 +243,11 @@ public async Task StopAsync() { try { - await session.DisconnectAsync(); + await session.DisposeAsync(); } catch (Exception ex) { - errors.Add(new Exception($"Failed to disconnect session {session.SessionId}: {ex.Message}", ex)); + errors.Add(new Exception($"Failed to dispose session {session.SessionId}: {ex.Message}", ex)); } } @@ -669,7 +669,7 @@ public async Task> ListModelsAsync(CancellationToken cancellatio /// A task that represents the asynchronous delete operation. /// Thrown when the session does not exist or deletion fails. /// - /// Unlike , which only releases in-memory + /// Unlike , which only releases in-memory /// resources and preserves session data for later resumption, this method is /// irreversible. The session cannot be resumed after deletion. /// diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index af175066..054f1097 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -26,7 +26,7 @@ namespace GitHub.Copilot.SDK; /// /// /// implements . Use the -/// await using pattern for automatic cleanup, or call +/// await using pattern for automatic cleanup, or call /// explicitly. Disposing a session releases in-memory resources but preserves session data /// on disk — the conversation can be resumed later via /// . To permanently delete session data, @@ -530,11 +530,10 @@ public async Task SetModelAsync(string model, CancellationToken cancellationToke } /// - /// Disconnects this session and releases all in-memory resources (event handlers, + /// Closes this session and releases all in-memory resources (event handlers, /// tool handlers, permission handlers). /// - /// A that can be used to cancel the operation. - /// A task representing the disconnect operation. + /// A task representing the dispose operation. /// /// /// Session state on disk (conversation history, planning state, artifacts) is @@ -549,14 +548,16 @@ public async Task SetModelAsync(string model, CancellationToken cancellationToke /// /// /// - /// // Disconnect when done — session can still be resumed later - /// await session.DisconnectAsync(); - /// - /// // Or use 'await using' for automatic disconnection + /// // 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(); /// /// - public async Task DisconnectAsync(CancellationToken cancellationToken = default) + public async ValueTask DisposeAsync() { if (Interlocked.Exchange(ref _isDisposed, 1) == 1) { @@ -566,7 +567,7 @@ public async Task DisconnectAsync(CancellationToken cancellationToken = default) try { await InvokeRpcAsync( - "session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }], cancellationToken); + "session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }], CancellationToken.None); } catch (ObjectDisposedException) { @@ -583,33 +584,6 @@ await InvokeRpcAsync( _permissionHandler = null; } - /// - /// Disposes the by disconnecting and releasing all resources. - /// - /// A task representing the dispose operation. - /// - /// - /// This method calls to perform cleanup. It is the - /// implementation of and enables the - /// await using pattern for automatic resource management. - /// - /// - /// - /// - /// // 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(); - /// - /// - public async ValueTask DisposeAsync() - { - await DisconnectAsync(); - } - internal record SendMessageRequest { public string SessionId { get; init; } = string.Empty;