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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/troubleshooting/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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 |
| Reset session | `session.reset(config)` | Abandon current runtime session and return a fresh one from explicit config; host apps clear their own UI |
| Disconnect session | `disconnect()` | Release in-memory resources |
| Destroy session *(deprecated)* | `destroy()` | Use `disconnect()` instead |
| Delete session | `deleteSession()` | Remove from storage |
Expand Down Expand Up @@ -99,7 +100,7 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b
| Export to file | `--share`, `/share` | Not in protocol |
| Export to gist | `--share-gist`, `/share gist` | Not in protocol |
| **Interactive UI** | | |
| Slash commands | `/help`, `/clear`, `/exit`, etc. | TUI-only |
| Slash commands | `/help`, `/exit`, etc. | TUI-only; use `session.reset(config)` for the lifecycle portion of `/clear` / `/reset` behavior |
| Agent picker dialog | `/agent` | Interactive UI |
| Diff mode dialog | `/diff` | Interactive UI |
| Feedback dialog | `/feedback` | Interactive UI |
Expand Down Expand Up @@ -137,7 +138,6 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b
| Logout | `/logout`, `copilot auth logout` | Direct CLI |
| User info | `/user` | TUI command |
| **Session Operations** | | |
| Clear conversation | `/clear` | TUI-only |
| Plan view | `/plan` | TUI-only (use SDK `session.rpc.plan.*` instead) |
| Session management | `/session`, `/resume`, `/rename` | TUI workflow |
| Fleet mode (interactive) | `/fleet` | TUI-only (use SDK `session.rpc.fleet.start()` instead) |
Expand Down
16 changes: 16 additions & 0 deletions dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,22 @@ When the user types `/deploy staging` in the CLI, the SDK receives a `command.ex

Commands are sent to the CLI on both `CreateSessionAsync` and `ResumeSessionAsync`, so you can update the command set when resuming.

## Resetting a Session

Use `session.ResetAsync(config)` to abandon the current runtime session and create a fresh session from explicit configuration. This mirrors the SDK-owned lifecycle part of the CLI TUI `/clear` command; `/reset` is its alias in the TUI.

```csharp
var result = await session.ResetAsync(new SessionConfig
{
Model = "gpt-5",
OnPermissionRequest = PermissionHandler.ApproveAll,
});
session = result.Session;
// Clear your app's visible transcript, local drafts, and route state here.
```

The returned `PreviousSessionId` identifies the abandoned session. The old `CopilotSession` object is closed after a successful reset, and the new session starts unnamed. If reset fails after teardown starts, treat the old session as no longer usable and create or resume another session explicitly. Host applications own UI cleanup and event listener rebinding.

## UI Elicitation

When the session has elicitation support — either from the CLI's TUI or from another client that registered an `OnElicitationRequest` handler (see [Elicitation Requests](#elicitation-requests)) — the SDK can request interactive form dialogs from the user. The `session.Ui` object provides convenience methods built on a single generic elicitation RPC.
Expand Down
50 changes: 49 additions & 1 deletion dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
/// <see cref="CopilotClient"/> that has not been explicitly disposed or removed.
/// </remarks>
internal readonly ConcurrentDictionary<string, CopilotSession> _sessions = new();
private readonly ConcurrentDictionary<string, byte> _resettingSessions = new();

private readonly CopilotClientOptions _options;
private readonly RuntimeConnection _connection;
Expand Down Expand Up @@ -345,6 +346,7 @@ public async Task StopAsync()
}

_sessions.Clear();
_resettingSessions.Clear();

await CleanupConnectionAsync(errors);

Expand Down Expand Up @@ -376,6 +378,7 @@ public async Task StopAsync()
public async Task ForceStopAsync()
{
_sessions.Clear();
_resettingSessions.Clear();

var errors = new List<Exception>();
await CleanupConnectionAsync(errors);
Expand Down Expand Up @@ -1153,6 +1156,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes

await UpdateSessionOptionsForModeAsync(session, config, cancellationToken).ConfigureAwait(false);
}

catch (Exception ex)
{
session.RemoveFromClient();
Expand All @@ -1173,6 +1177,41 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
return session;
}

internal async Task<ResetSessionResult> ResetSessionAsync(
CopilotSession session,
SessionConfig config,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(session);
ArgumentNullException.ThrowIfNull(config);

var previousSessionId = session.SessionId;
if (!_sessions.TryGetValue(previousSessionId, out var trackedSession) || !ReferenceEquals(trackedSession, session))
{
throw new InvalidOperationException($"Cannot reset session {previousSessionId}: it is not active on this client.");
}

if (!_resettingSessions.TryAdd(previousSessionId, 0))
{
throw new InvalidOperationException($"Cannot reset session {previousSessionId}: reset is already in progress.");
}

try
{
await session.Rpc.Queue.ClearAsync(cancellationToken).ConfigureAwait(false);
await session.DestroyForResetAsync(cancellationToken).ConfigureAwait(false);

var resetConfig = config.Clone();
resetConfig.SessionId = null;
var freshSession = await CreateSessionAsync(resetConfig, cancellationToken).ConfigureAwait(false);
return new ResetSessionResult(previousSessionId, freshSession);
}
finally
{
_resettingSessions.TryRemove(previousSessionId, out _);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@redsun82 Can you clarify if there's some part of the logic here that needs to be done inside the SDK as opposed to being done by client apps?

I would have thought client apps are already able to destroy a session and then create a new one with equivalent config. What extra thing do we get from having ResetSessionAsync as an SDK feature?

I'm asking because this is quite an opinionated flow - it's not at all obvious that "reset" should actually mean "destroy and get a different instance". From the name alone I think people would expect "reset" to mutate the session in some way that resets it (so you end up with the same session object, same session ID), not to create a whole new session with a new ID.

So I'm wondering if we could avoid this risk of confusion by keeping this logic inside the apps that need it, rather than making it a general SDK feature.

@redsun82 redsun82 Jun 12, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

hey @SteveSandersonMS thanks for the feedback, this is a point I also hesitated a lot on.

My thinking here is that apps can technically do destroy + create, but it turns out there is a bunch of SDK-owned state around that sequence: pending request queues, per-session handlers/delegates, active-session maps, stale handles, and concurrent reset guards, and apps can get that subtly wrong.

As for whether it should be an in-place method: that would have been my go-to as well, it's just that it's not the current behaviour of the CLI's /clear. But maybe we should turn this around, make the most natural addition to the SDK and just slightly change /clear's semantics to go with that?

I remain very open to suggestions! For context, my main objective is to add /clear to the Copilot desktop app, so the more I'm able to share through the SDK the happier I am, but we also don't want to muddy the SDK just for that.


/// <summary>
/// Validates the health of the connection by sending a ping request.
/// </summary>
Expand Down Expand Up @@ -1331,6 +1370,7 @@ public async Task DeleteSessionAsync(string sessionId, CancellationToken cancell
}

RemoveSession(sessionId);
_resettingSessions.TryRemove(sessionId, out _);
}

/// <summary>
Expand Down Expand Up @@ -2074,6 +2114,11 @@ private void RemoveSession(string sessionId)
_sessions.TryRemove(sessionId, out _);
}

internal void RemoveSession(CopilotSession session)
{
((ICollection<KeyValuePair<string, CopilotSession>>)_sessions).Remove(new(session.SessionId, session));
}

/// <summary>
/// Disposes the <see cref="CopilotClient"/> synchronously.
/// </summary>
Expand Down Expand Up @@ -2245,7 +2290,10 @@ private async Task PumpAsync(Process process, ILogger logger, CancellationToken
Buffer.AppendLine(line);
}

logger.LogWarning("[CLI] {Line}", line);
if (logger.IsEnabled(LogLevel.Warning))
{
logger.LogWarning("[CLI] {Line}", line);
}
}
}
catch (Exception e) when (cancellationToken.IsCancellationRequested
Expand Down
5 changes: 4 additions & 1 deletion dotnet/src/JsonRpc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,10 @@ private void HandleResponse(JsonElement message, JsonElement idProp)
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Inline response callback for request {RequestId} threw", id);
if (_logger.IsEnabled(LogLevel.Warning))
{
_logger.LogWarning(ex, "Inline response callback for request {RequestId} threw", id);
}
pending.TrySetException(ex);
return;
}
Expand Down
50 changes: 49 additions & 1 deletion dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ internal CopilotSession(
/// </summary>
internal void RemoveFromClient()
{
((ICollection<KeyValuePair<string, CopilotSession>>)_parentClient._sessions).Remove(new(SessionId, this));
_parentClient.RemoveSession(this);
}

internal void StartProcessingEvents()
Expand Down Expand Up @@ -1729,6 +1729,35 @@ await InvokeRpcAsync<object>(
}

_eventHandlers = ImmutableInterlocked.InterlockedExchange(ref _eventHandlers, ImmutableArray<EventSubscription>.Empty);
ClearLocalState();
}

internal async Task DestroyForResetAsync(CancellationToken cancellationToken)
{
if (Interlocked.Exchange(ref _isDisposed, 1) == 1)
{
return;
}

try
{
await InvokeRpcAsync<object>(
"session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }], cancellationToken);
}
catch
{
Interlocked.Exchange(ref _isDisposed, 0);
throw;
}

_eventChannel.Writer.TryComplete();
RemoveFromClient();
_eventHandlers = ImmutableInterlocked.InterlockedExchange(ref _eventHandlers, ImmutableArray<EventSubscription>.Empty);
ClearLocalState();
}

private void ClearLocalState()
{
_toolHandlers.Clear();
_commandHandlers.Clear();

Expand All @@ -1739,6 +1768,25 @@ await InvokeRpcAsync<object>(
_autoModeSwitchHandler = null;
}

/// <summary>
/// Resets this conversation by closing the underlying runtime session and
/// creating a fresh session from <paramref name="config"/>.
/// </summary>
/// <remarks>
/// Use the returned session for subsequent work. The SDK does not clear
/// host-owned UI state, local drafts, or app persistence. If reset fails
/// after teardown starts, treat the old session as no longer usable and
/// create or resume another session explicitly.
/// </remarks>
/// <param name="config">Configuration for the replacement session. Any <see cref="SessionConfig.SessionId"/> is ignored.</param>
/// <param name="cancellationToken">A token to cancel the reset operation.</param>
/// <returns>The fresh session and the previous session ID.</returns>
public Task<ResetSessionResult> ResetAsync(SessionConfig config, CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
return _parentClient.ResetSessionAsync(this, config, cancellationToken);
}

[LoggerMessage(Level = LogLevel.Error, Message = "Unhandled exception in broadcast event handler")]
private partial void LogBroadcastHandlerError(Exception exception);

Expand Down
7 changes: 7 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2928,6 +2928,13 @@ private SessionConfig(SessionConfig? other) : base(other)
public SessionConfig Clone() => new(this);
}

/// <summary>
/// Result returned by <see cref="CopilotSession.ResetAsync"/>.
/// </summary>
/// <param name="PreviousSessionId">The session ID that was closed and replaced.</param>
/// <param name="Session">The fresh session created from the supplied reset configuration.</param>
public sealed record ResetSessionResult(string PreviousSessionId, CopilotSession Session);

/// <summary>
/// Configuration options for resuming an existing Copilot session.
/// </summary>
Expand Down
18 changes: 18 additions & 0 deletions go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,24 @@ session, err := client.ResumeSession(ctx, sessionID, &copilot.ResumeSessionConfi

If a handler returns an error, the SDK sends the error message back to the server. Unknown commands automatically receive an error response.

## Resetting a Session

Use `session.Reset(ctx, config)` to abandon the current runtime session and create a fresh session from explicit configuration. This mirrors the SDK-owned lifecycle part of the CLI TUI `/clear` command; `/reset` is its alias in the TUI.

```go
result, err := session.Reset(ctx, &copilot.SessionConfig{
Model: "gpt-5",
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
})
if err != nil {
return err
}
session = result.Session
// Clear your app's visible transcript, local drafts, and route state here.
```

The returned `PreviousSessionID` identifies the abandoned session. The old `Session` object is closed after a successful reset, and the new session starts unnamed. If reset fails after teardown starts, treat the old session as no longer usable and create or resume another session explicitly. Host applications own UI cleanup and event listener rebinding.

## UI Elicitation

The SDK provides convenience methods to ask the user questions via elicitation dialogs. These are gated by host capabilities — check `session.Capabilities().UI.Elicitation` before calling.
Expand Down
Loading
Loading