diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs
index 0e1ec145e..97745e78b 100644
--- a/dotnet/src/Client.cs
+++ b/dotnet/src/Client.cs
@@ -174,6 +174,8 @@ public CopilotClient(CopilotClientOptions? options = null)
throw new ArgumentException($"Unsupported RuntimeConnection type: {_connection.GetType().Name}", nameof(options));
}
+ ValidateEnvironmentOptions(_options, _connection);
+
_logger = _options.Logger ?? NullLogger.Instance;
_onListModels = _options.OnListModels;
@@ -202,6 +204,53 @@ _options.SessionFs is not null ||
}
}
+ ///
+ /// Validates environment-variable options against the resolved transport.
+ /// Per-client environment is only representable for child-process transports
+ /// (each client owns its own OS process). The in-process (FFI) transport
+ /// loads the native runtime into the shared host process, whose single
+ /// environment block cannot carry per-client values, so environment and
+ /// telemetry options that lower to environment variables are rejected there.
+ ///
+ private static void ValidateEnvironmentOptions(CopilotClientOptions options, RuntimeConnection connection)
+ {
+ if (connection is InProcessRuntimeConnection)
+ {
+ if (options.Environment is not null)
+ {
+ throw new ArgumentException(
+ $"{nameof(CopilotClientOptions)}.{nameof(CopilotClientOptions.Environment)} is not supported with " +
+ $"{nameof(RuntimeConnection)}.{nameof(RuntimeConnection.ForInProcess)}(): the in-process transport " +
+ "loads the native runtime into the shared host process, whose single environment block cannot carry " +
+ "per-client values. Set the variables on the host process environment instead.",
+ nameof(options));
+ }
+
+ if (options.Telemetry is not null)
+ {
+ throw new ArgumentException(
+ $"{nameof(CopilotClientOptions)}.{nameof(CopilotClientOptions.Telemetry)} is not supported with " +
+ $"{nameof(RuntimeConnection)}.{nameof(RuntimeConnection.ForInProcess)}(): telemetry configuration is " +
+ "lowered to environment variables read by native runtime code running in the shared host process, so " +
+ "per-client telemetry cannot be honored in-process. Configure telemetry via the host process " +
+ "environment, or use a child-process transport.",
+ nameof(options));
+ }
+
+ return;
+ }
+
+ if (connection is ChildProcessRuntimeConnection { Environment: not null } && options.Environment is not null)
+ {
+ throw new ArgumentException(
+ $"Set environment variables via either {nameof(CopilotClientOptions)}.{nameof(CopilotClientOptions.Environment)} " +
+ $"or {nameof(ChildProcessRuntimeConnection)}.{nameof(ChildProcessRuntimeConnection.Environment)}, not both. " +
+ $"Prefer {nameof(ChildProcessRuntimeConnection)}.{nameof(ChildProcessRuntimeConnection.Environment)} for " +
+ "child-process transports.",
+ nameof(options));
+ }
+ }
+
///
/// Environment variable that overrides the transport used when the caller does not
/// specify . Accepts "inprocess"
@@ -1943,9 +1992,12 @@ private static void ApplyTelemetryEnvironment(IDictionary envir
var tcpConnection = _connection as TcpRuntimeConnection;
var useStdio = _connection is StdioRuntimeConnection;
- // Use explicit path, COPILOT_CLI_PATH env var (from options.Environment or process env), or bundled runtime - no PATH fallback
- var envCliPath = options.Environment is not null && options.Environment.TryGetValue("COPILOT_CLI_PATH", out var envValue) ? envValue
- : System.Environment.GetEnvironmentVariable("COPILOT_CLI_PATH");
+ // Use explicit path, COPILOT_CLI_PATH env var (from the connection's
+ // Environment, options.Environment, or process env), or bundled runtime - no PATH fallback
+ var envCliPath =
+ (childProcessConnection.Environment is not null && childProcessConnection.Environment.TryGetValue("COPILOT_CLI_PATH", out var connEnvValue) ? connEnvValue : null)
+ ?? (options.Environment is not null && options.Environment.TryGetValue("COPILOT_CLI_PATH", out var envValue) ? envValue : null)
+ ?? System.Environment.GetEnvironmentVariable("COPILOT_CLI_PATH");
var cliPath = childProcessConnection.Path
?? envCliPath
?? GetBundledCliPath(out var searchedPath)
@@ -2012,10 +2064,11 @@ private static void ApplyTelemetryEnvironment(IDictionary envir
CreateNoWindow = true
};
- if (options.Environment != null)
+ var childEnvironment = options.Environment ?? childProcessConnection.Environment;
+ if (childEnvironment != null)
{
startInfo.Environment.Clear();
- foreach (var (key, value) in options.Environment)
+ foreach (var (key, value) in childEnvironment)
{
startInfo.Environment[key] = value;
}
diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs
index bcd8903c8..65295d3d2 100644
--- a/dotnet/src/Types.cs
+++ b/dotnet/src/Types.cs
@@ -173,6 +173,16 @@ internal ChildProcessRuntimeConnection() { }
/// Extra command-line arguments to pass to the runtime process.
public IList? Args { get; set; }
+
+ ///
+ /// Gets or sets the environment variables passed to the spawned runtime process,
+ /// replacing the inherited environment.
+ ///
+ ///
+ /// Cannot be combined with ; setting both throws
+ /// an when the client is constructed.
+ ///
+ public IReadOnlyDictionary? Environment { get; set; }
}
///
@@ -358,7 +368,15 @@ private CopilotClientOptions(CopilotClientOptions? other)
///
public CopilotLogLevel? LogLevel { get; set; }
- /// Environment variables to pass to the runtime process.
+ ///
+ /// Gets or sets environment variables passed to the runtime process.
+ ///
+ ///
+ /// Not supported with the in-process transport (),
+ /// which runs the runtime in the host process; setting this option there throws an
+ /// . For child-process transports, prefer
+ /// ; setting both throws.
+ ///
public IReadOnlyDictionary? Environment { get; set; }
/// Logger instance for SDK diagnostic output.
diff --git a/dotnet/test/ConnectionTokenTests.cs b/dotnet/test/ConnectionTokenTests.cs
index 524ff2586..3192bada6 100644
--- a/dotnet/test/ConnectionTokenTests.cs
+++ b/dotnet/test/ConnectionTokenTests.cs
@@ -113,7 +113,7 @@ public class ConnectionTokenAutoGeneratedTests : IAsyncLifetime
public async Task InitializeAsync()
{
_ctx = await E2ETestContext.CreateAsync();
- _client = _ctx.CreateClient(useStdio: false);
+ _client = _ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForTcp() });
}
public async Task DisposeAsync()
diff --git a/dotnet/test/E2E/ClientOptionsE2ETests.cs b/dotnet/test/E2E/ClientOptionsE2ETests.cs
index d86b6e477..a8ceda5b0 100644
--- a/dotnet/test/E2E/ClientOptionsE2ETests.cs
+++ b/dotnet/test/E2E/ClientOptionsE2ETests.cs
@@ -71,7 +71,6 @@ public async Task Should_Propagate_Process_Options_To_Spawned_Cli()
{
Connection = RuntimeConnection.ForStdio(path: cliPath, args: ["--capture-file", capturePath]),
BaseDirectory = copilotHomeFromOption,
- Environment = clientEnv,
GitHubToken = "process-option-token",
LogLevel = CopilotLogLevel.Debug,
SessionIdleTimeoutSeconds = 17,
@@ -85,7 +84,7 @@ public async Task Should_Propagate_Process_Options_To_Spawned_Cli()
CaptureContent = true,
},
UseLoggedInUser = false,
- });
+ }, environment: clientEnv);
await client.StartAsync();
diff --git a/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs b/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs
index 464aadb66..9bb19df7d 100644
--- a/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs
+++ b/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs
@@ -51,8 +51,7 @@ public async Task Services_A_WebSocket_Turn_End_To_End_Via_The_Request_Handler()
{
Connection = RuntimeConnection.ForStdio(),
RequestHandler = handler,
- Environment = env,
- });
+ }, environment: env);
await client.StartAsync();
var session = await client.CreateSessionAsync(new SessionConfig
diff --git a/dotnet/test/E2E/ModeHandlersE2ETests.cs b/dotnet/test/E2E/ModeHandlersE2ETests.cs
index 40552fa9f..a397999f7 100644
--- a/dotnet/test/E2E/ModeHandlersE2ETests.cs
+++ b/dotnet/test/E2E/ModeHandlersE2ETests.cs
@@ -155,7 +155,7 @@ private CopilotClient CreateAuthenticatedClient()
["COPILOT_DEBUG_GITHUB_API_URL"] = Ctx.ProxyUrl,
};
- return Ctx.CreateClient(options: new CopilotClientOptions { Environment = env });
+ return Ctx.CreateClient(environment: env);
}
private Task ConfigureAuthenticatedUserAsync()
diff --git a/dotnet/test/E2E/PerSessionAuthE2ETests.cs b/dotnet/test/E2E/PerSessionAuthE2ETests.cs
index 7e104b33d..d1226f373 100644
--- a/dotnet/test/E2E/PerSessionAuthE2ETests.cs
+++ b/dotnet/test/E2E/PerSessionAuthE2ETests.cs
@@ -22,7 +22,7 @@ private CopilotClient CreateAuthTestClient()
};
// Disable the harness's auto-injected client token so the per-session
// auth tests validate only session-scoped tokens.
- return Ctx.CreateClient(options: new CopilotClientOptions { Environment = env }, autoInjectGitHubToken: false);
+ return Ctx.CreateClient(environment: env, autoInjectGitHubToken: false);
}
private CopilotClient CreateNoAuthTestClient()
@@ -32,9 +32,8 @@ private CopilotClient CreateNoAuthTestClient()
return Ctx.CreateClient(options: new CopilotClientOptions
{
- Environment = env,
UseLoggedInUser = false,
- }, autoInjectGitHubToken: false);
+ }, autoInjectGitHubToken: false, environment: env);
}
private static Dictionary WithoutAuthEnv(Dictionary env)
diff --git a/dotnet/test/E2E/ProviderEndpointE2ETests.cs b/dotnet/test/E2E/ProviderEndpointE2ETests.cs
index f7e9a7885..7ea4eecf3 100644
--- a/dotnet/test/E2E/ProviderEndpointE2ETests.cs
+++ b/dotnet/test/E2E/ProviderEndpointE2ETests.cs
@@ -23,7 +23,7 @@ private CopilotClient CreateProviderEndpointClient()
{
["COPILOT_ALLOW_GET_PROVIDER_ENDPOINT"] = "true",
};
- return Ctx.CreateClient(options: new CopilotClientOptions { Environment = env });
+ return Ctx.CreateClient(environment: env);
}
[Fact]
diff --git a/dotnet/test/E2E/RpcExtensionsLoadedE2ETests.cs b/dotnet/test/E2E/RpcExtensionsLoadedE2ETests.cs
index c1e43b09e..af959bd7e 100644
--- a/dotnet/test/E2E/RpcExtensionsLoadedE2ETests.cs
+++ b/dotnet/test/E2E/RpcExtensionsLoadedE2ETests.cs
@@ -58,8 +58,7 @@ private CopilotClient CreateExtensionsClient()
return Ctx.CreateClient(options: new CopilotClientOptions
{
Connection = RuntimeConnection.ForStdio(args: ["--yolo"]),
- Environment = ExtensionsEnabledEnvironment(),
- });
+ }, environment: ExtensionsEnabledEnvironment());
}
///
diff --git a/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs b/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs
index d53f93c9a..350aac427 100644
--- a/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs
+++ b/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs
@@ -398,10 +398,7 @@ private CopilotClient CreateMcpAppsClient()
environment["COPILOT_MCP_APPS"] = "true";
environment["MCP_APPS"] = "true";
- return Ctx.CreateClient(options: new CopilotClientOptions
- {
- Environment = environment,
- });
+ return Ctx.CreateClient(environment: environment);
}
private static void CreateSkill(string skillsDir, string skillName, string description)
diff --git a/dotnet/test/E2E/RpcServerE2ETests.cs b/dotnet/test/E2E/RpcServerE2ETests.cs
index 1f240af9e..47df25615 100644
--- a/dotnet/test/E2E/RpcServerE2ETests.cs
+++ b/dotnet/test/E2E/RpcServerE2ETests.cs
@@ -37,9 +37,8 @@ private CopilotClient CreateAuthenticatedClient(string token)
return Ctx.CreateClient(options: new CopilotClientOptions
{
- Environment = env,
GitHubToken = token,
- });
+ }, environment: env);
}
private async Task ConfigureAuthenticatedUserAsync(
@@ -237,10 +236,7 @@ public async Task Should_Add_Secret_Filter_Values()
{
var environment = Ctx.GetEnvironment();
environment["COPILOT_ENABLE_SECRET_FILTERING"] = "true";
- await using var client = Ctx.CreateClient(options: new CopilotClientOptions
- {
- Environment = environment,
- });
+ await using var client = Ctx.CreateClient(environment: environment);
await client.StartAsync();
var secret = $"rpc-secret-{Guid.NewGuid():N}";
@@ -381,7 +377,7 @@ public async Task Should_Check_In_Use_Session_From_Another_Runtime_And_Release_L
{
var sessionId = Guid.NewGuid().ToString();
var workingDirectory = CreateUniqueWorkDirectory("server-rpc-in-use");
- await using var otherClient = Ctx.CreateClient(useStdio: true);
+ await using var otherClient = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForStdio() });
await using var otherSession = await otherClient.CreateSessionAsync(new SessionConfig
{
SessionId = sessionId,
diff --git a/dotnet/test/E2E/RpcServerMiscE2ETests.cs b/dotnet/test/E2E/RpcServerMiscE2ETests.cs
index 6cb21f75d..29e560100 100644
--- a/dotnet/test/E2E/RpcServerMiscE2ETests.cs
+++ b/dotnet/test/E2E/RpcServerMiscE2ETests.cs
@@ -235,7 +235,7 @@ public async Task Should_Reject_Send_Attachments_From_Non_Extension_Connection()
env["GITHUB_TOKEN"] = "";
}
- var options = new CopilotClientOptions { Environment = env };
+ var options = new CopilotClientOptions();
if (!autoInjectGitHubToken)
{
options.UseLoggedInUser = false;
@@ -243,7 +243,8 @@ public async Task Should_Reject_Send_Attachments_From_Non_Extension_Connection()
var client = Ctx.CreateClient(
options: options,
- autoInjectGitHubToken: autoInjectGitHubToken);
+ autoInjectGitHubToken: autoInjectGitHubToken,
+ environment: env);
await client.StartAsync();
return (client, home);
}
diff --git a/dotnet/test/E2E/RpcServerPluginsE2ETests.cs b/dotnet/test/E2E/RpcServerPluginsE2ETests.cs
index eea409593..64a0f1c26 100644
--- a/dotnet/test/E2E/RpcServerPluginsE2ETests.cs
+++ b/dotnet/test/E2E/RpcServerPluginsE2ETests.cs
@@ -302,7 +302,7 @@ This skill exists so the plugin reports at least one installed skill.
env["XDG_CONFIG_HOME"] = home;
env["XDG_STATE_HOME"] = home;
- var client = Ctx.CreateClient(options: new CopilotClientOptions { Environment = env });
+ var client = Ctx.CreateClient(environment: env);
await client.StartAsync();
return (client, home);
}
diff --git a/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs b/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs
index 130d2e468..f3f3d5d5d 100644
--- a/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs
+++ b/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs
@@ -313,9 +313,8 @@ private CopilotClient CreateAuthenticatedClient(string token)
return Ctx.CreateClient(options: new CopilotClientOptions
{
- Environment = env,
GitHubToken = token,
- });
+ }, environment: env);
}
private async Task ConfigureAuthenticatedUserAsync(string token)
diff --git a/dotnet/test/E2E/SessionFsSqliteE2ETests.cs b/dotnet/test/E2E/SessionFsSqliteE2ETests.cs
index 1e6175f9c..bf7bdf3b5 100644
--- a/dotnet/test/E2E/SessionFsSqliteE2ETests.cs
+++ b/dotnet/test/E2E/SessionFsSqliteE2ETests.cs
@@ -117,9 +117,9 @@ await TestHelper.WaitForConditionAsync(
private CopilotClient CreateSessionFsClient()
{
return Ctx.CreateClient(
- useStdio: true,
options: new CopilotClientOptions
{
+ Connection = RuntimeConnection.ForStdio(),
SessionFs = SessionFsConfig,
});
}
diff --git a/dotnet/test/E2E/SubagentHooksE2ETests.cs b/dotnet/test/E2E/SubagentHooksE2ETests.cs
index 23ba8ed3a..4723c19b7 100644
--- a/dotnet/test/E2E/SubagentHooksE2ETests.cs
+++ b/dotnet/test/E2E/SubagentHooksE2ETests.cs
@@ -20,7 +20,7 @@ public async Task Should_Invoke_PreToolUse_And_PostToolUse_Hooks_For_Sub_Agent_T
// Create a client with the session-based subagents feature flag
var env = new Dictionary(Ctx.GetEnvironment());
env["COPILOT_EXP_COPILOT_CLI_SESSION_BASED_SUBAGENTS"] = "true";
- var client = Ctx.CreateClient(options: new CopilotClientOptions { Environment = env });
+ var client = Ctx.CreateClient(environment: env);
var session = await client.CreateSessionAsync(new SessionConfig
{
diff --git a/dotnet/test/E2E/TelemetryExportE2ETests.cs b/dotnet/test/E2E/TelemetryExportE2ETests.cs
index 22ed5663d..2dd9d591a 100644
--- a/dotnet/test/E2E/TelemetryExportE2ETests.cs
+++ b/dotnet/test/E2E/TelemetryExportE2ETests.cs
@@ -24,6 +24,7 @@ public async Task Should_Export_File_Telemetry_For_Sdk_Interactions()
await using var client = Ctx.CreateClient(options: new CopilotClientOptions
{
+ Connection = RuntimeConnection.ForStdio(),
Telemetry = new TelemetryConfig
{
FilePath = telemetryPath,
diff --git a/dotnet/test/Harness/E2ETestContext.cs b/dotnet/test/Harness/E2ETestContext.cs
index 99c58c92c..f4df1749f 100644
--- a/dotnet/test/Harness/E2ETestContext.cs
+++ b/dotnet/test/Harness/E2ETestContext.cs
@@ -237,61 +237,75 @@ public Dictionary GetEnvironment()
}
public CopilotClient CreateClient(
- bool? useStdio = null,
CopilotClientOptions? options = null,
bool autoInjectGitHubToken = true,
- bool persistent = false)
+ bool persistent = false,
+ IReadOnlyDictionary? environment = null)
{
options ??= new CopilotClientOptions();
options.WorkingDirectory ??= WorkDir;
- options.Environment ??= GetEnvironment();
options.Logger ??= Logger;
- // Build the connection. If the caller supplied one, just ensure the runtime path is set.
- // When neither a Connection nor useStdio is specified, leave Connection null so
- // CopilotClient honors COPILOT_SDK_DEFAULT_CONNECTION (defaulting to stdio); useStdio
- // is a convenience shortcut to pin stdio/tcp. Passing both a Connection and useStdio is ambiguous.
- if (useStdio is not null && options.Connection is not null)
+ // Tests must supply environment via the 'environment' parameter, which the
+ // harness routes to the right place per transport (the connection for
+ // child-process transports, the host process for in-process). Setting
+ // options.Environment directly bypasses that routing and is unsupported
+ // in-process, so reject it here.
+ if (options.Environment is not null)
{
throw new ArgumentException(
- "Specify either useStdio or options.Connection, not both. " +
- "Use options.Connection (e.g. RuntimeConnection.ForStdio() / RuntimeConnection.ForTcp()) to control transport when supplying a Connection.",
- nameof(useStdio));
+ "Do not set options.Environment in E2E tests; pass the 'environment' parameter to CreateClient instead.",
+ nameof(options));
}
+ // The full environment the client runs with: harness defaults (proxy
+ // redirect, isolated home, cleared HMAC/tokens, etc.) unless the test
+ // supplied a complete replacement.
+ var env = environment is not null
+ ? environment.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
+ : GetEnvironment();
+
+ // When the test doesn't pin a transport, leave Connection null so
+ // CopilotClient honors COPILOT_SDK_DEFAULT_CONNECTION (stdio by default,
+ // or in-process); the CI matrix uses this to run the suite under both.
+ // Tests that need a specific transport set options.Connection directly.
var cliPath = GetCliPath(_repoRoot);
switch (options.Connection)
{
- case null when useStdio == true:
+ case null when !IsInProcess(null):
+ // No explicit connection and not the in-process default: the
+ // default resolves to stdio, so materialize it here so the
+ // environment can be attached to the connection below.
options.Connection = RuntimeConnection.ForStdio(path: cliPath);
break;
- case null when useStdio == false:
- options.Connection = RuntimeConnection.ForTcp(path: cliPath);
- break;
case null:
- // useStdio is null: leave Connection unset so CopilotClient's
- // ResolveDefaultConnection honors COPILOT_SDK_DEFAULT_CONNECTION
- // (stdio by default, or in-process). The CLI path flows through
- // options.Environment["COPILOT_CLI_PATH"] (GetEnvironment copies
- // the process env, where CI's setup-copilot sets it).
+ // In-process default: leave Connection unset so CopilotClient's
+ // ResolveDefaultConnection honors COPILOT_SDK_DEFAULT_CONNECTION.
break;
case ChildProcessRuntimeConnection child when child.Path is null:
child.Path = cliPath;
break;
}
- // In-process hosting workaround: several runtime code paths run host-side
- // in this process (the loaded cdylib) and read the ambient process
- // environment rather than the environment passed to
- // copilot_runtime_host_start, so our per-test redirects, cleared tokens,
- // cleared HMAC keys, and isolated home in options.Environment are
- // invisible to them unless mirrored onto this process's real environment.
- // Restored after each test by InProcessEnvIsolationAttribute. Harmless for
- // child-process transports, which configure their child's environment.
- foreach (var (name, value) in options.Environment)
+ if (IsInProcess(options.Connection))
{
- InProcessEnvIsolation.Apply(name, value);
+ // In-process hosting: runtime code runs host-side in this process (the
+ // loaded cdylib) and reads the ambient process environment rather than
+ // the environment passed to copilot_runtime_host_start, so the per-test
+ // redirects, cleared tokens/HMAC, and isolated home must be mirrored
+ // onto this process's real environment. Restored after each test by
+ // InProcessEnvIsolationAttribute.
+ foreach (var (name, value) in env)
+ {
+ InProcessEnvIsolation.Apply(name, value);
+ }
+ }
+ else if (options.Connection is ChildProcessRuntimeConnection child)
+ {
+ // Child-process transport: hand the environment to the spawned child
+ // via the connection, where per-client environment is coherent.
+ child.Environment = env;
}
// Auto-inject auth token unless connecting to an existing runtime via URI.
@@ -440,6 +454,28 @@ private static async Task DeleteDirectoryAsync(string path)
}
}
+ ///
+ /// Determines whether the resolved transport is the in-process (FFI) host,
+ /// mirroring 's own default-connection resolution:
+ /// an explicit , or (when no connection
+ /// is given) the COPILOT_SDK_DEFAULT_CONNECTION=inprocess default.
+ ///
+ private static bool IsInProcess(RuntimeConnection? connection)
+ {
+ if (connection is InProcessRuntimeConnection)
+ {
+ return true;
+ }
+ if (connection is null)
+ {
+ return string.Equals(
+ Environment.GetEnvironmentVariable("COPILOT_SDK_DEFAULT_CONNECTION"),
+ "inprocess",
+ StringComparison.OrdinalIgnoreCase);
+ }
+ return false;
+ }
+
// Inproc holds the session-store SQLite handle in-process; graceful StopAsync releases it so the temp-dir delete succeeds on Windows.
private static async Task StopClientForCleanupAsync(CopilotClient client)
{