Skip to content

Commit b4ef955

Browse files
Add configurable session idle timeout option (#1093)
* Add sessionIdleTimeoutMs option to CopilotClientOptions Add a new optional sessionIdleTimeoutMs field to CopilotClientOptions that allows consumers to configure the server-wide session idle timeout. When set to a positive value, the SDK passes --session-idle-timeout to the CLI process. Sessions have no idle timeout by default (infinite lifetime). The minimum configurable value is 300000ms (5 minutes). Also updates the session persistence documentation to reflect the new default behavior and configuration option. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: rename sessionIdleTimeoutMs to sessionIdleTimeoutSeconds Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: correct @default tag for sessionIdleTimeoutSeconds Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: format client.ts with prettier Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add sessionIdleTimeoutSeconds to Go, Python, and .NET SDKs Add the session idle timeout option across all remaining SDK languages, consistent with the Node.js implementation and the runtime CLI's --session-idle-timeout flag. - Go: SessionIdleTimeoutSeconds int on ClientOptions - Python: session_idle_timeout_seconds on SubprocessConfig - .NET: SessionIdleTimeoutSeconds int? on CopilotClientOptions Each SDK passes --session-idle-timeout <seconds> to the CLI when the value is positive, and omits it otherwise (disabled by default). Includes unit tests for all three languages and updates the .NET clone test to cover the new property. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: add external-server caveat to Node.js and Python idle timeout docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: add Node.js unit tests for sessionIdleTimeoutSeconds Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: format test_client.py with ruff Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: remove incorrect minimum value from idle timeout docs The runtime does not enforce a minimum value for the session idle timeout - any positive value is accepted. Remove the 'Minimum value: 300 (5 minutes)' note from all SDK docstrings and docs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: copy SessionIdleTimeoutSeconds unconditionally in Go NewClient() The option was only copied when > 0, which silently normalized negative inputs. Other SDKs preserve the caller's value and gate only at spawn time. Align Go to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a3e273c commit b4ef955

13 files changed

Lines changed: 154 additions & 3 deletions

File tree

docs/features/session-persistence.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -433,14 +433,26 @@ await client.deleteSession("user-123-task-456");
433433
434434
## Automatic Cleanup: Idle Timeout
435435

436-
The CLI has a built-in 30-minute idle timeout. Sessions without activity are automatically cleaned up:
436+
By default, sessions have **no idle timeout** and live indefinitely until explicitly disconnected or deleted. You can optionally configure a server-wide idle timeout via `CopilotClientOptions.sessionIdleTimeoutSeconds`:
437+
438+
```typescript
439+
const client = new CopilotClient({
440+
sessionIdleTimeoutSeconds: 30 * 60, // 30 minutes
441+
});
442+
```
443+
444+
When a timeout is configured, sessions without activity for that duration are automatically cleaned up. Set to `0` or omit to disable.
445+
446+
> **Note:** This option only applies when the SDK spawns the runtime process. When connecting to an existing server via `cliUrl`, the server's own timeout configuration applies.
437447
438448
```mermaid
439449
flowchart LR
440-
A["⚡ Last Activity"] --> B["⏳ 25 min<br/>timeout_warning"] --> C["🧹 30 min<br/>destroyed"]
450+
A["⚡ Last Activity"] --> B["⏳ ~5 min before<br/>timeout_warning"] --> C["🧹 Timeout<br/>destroyed"]
441451
```
442452

443-
Listen for idle events to know when work completes:
453+
Sessions with active work (running commands, background agents) are always protected from idle cleanup, regardless of the timeout setting.
454+
455+
Listen for idle events to react to session inactivity:
444456

445457
```typescript
446458
session.on("session.idle", (event) => {

dotnet/src/Client.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,11 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
11901190
args.Add("--no-auto-login");
11911191
}
11921192

1193+
if (options.SessionIdleTimeoutSeconds is > 0)
1194+
{
1195+
args.AddRange(["--session-idle-timeout", options.SessionIdleTimeoutSeconds.Value.ToString(CultureInfo.InvariantCulture)]);
1196+
}
1197+
11931198
var (fileName, processArgs) = ResolveCliCommand(cliPath, args);
11941199

11951200
var startInfo = new ProcessStartInfo

dotnet/src/Types.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ protected CopilotClientOptions(CopilotClientOptions? other)
6969
UseStdio = other.UseStdio;
7070
OnListModels = other.OnListModels;
7171
SessionFs = other.SessionFs;
72+
SessionIdleTimeoutSeconds = other.SessionIdleTimeoutSeconds;
7273
}
7374

7475
/// <summary>
@@ -165,6 +166,15 @@ public string? GithubToken
165166
/// </summary>
166167
public TelemetryConfig? Telemetry { get; set; }
167168

169+
/// <summary>
170+
/// Server-wide idle timeout for sessions in seconds.
171+
/// Sessions without activity for this duration are automatically cleaned up.
172+
/// Set to <c>0</c> or leave as <see langword="null"/> to disable (sessions live indefinitely).
173+
/// This option is only used when the SDK spawns the CLI process; it is ignored
174+
/// when connecting to an external server via <see cref="CliUrl"/>.
175+
/// </summary>
176+
public int? SessionIdleTimeoutSeconds { get; set; }
177+
168178
/// <summary>
169179
/// Creates a shallow clone of this <see cref="CopilotClientOptions"/> instance.
170180
/// </summary>

dotnet/test/ClientTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,25 @@ public void Should_Throw_When_UseLoggedInUser_Used_With_CliUrl()
216216
});
217217
}
218218

219+
[Fact]
220+
public void Should_Default_SessionIdleTimeoutSeconds_To_Null()
221+
{
222+
var options = new CopilotClientOptions();
223+
224+
Assert.Null(options.SessionIdleTimeoutSeconds);
225+
}
226+
227+
[Fact]
228+
public void Should_Accept_SessionIdleTimeoutSeconds_Option()
229+
{
230+
var options = new CopilotClientOptions
231+
{
232+
SessionIdleTimeoutSeconds = 600
233+
};
234+
235+
Assert.Equal(600, options.SessionIdleTimeoutSeconds);
236+
}
237+
219238
[Fact]
220239
public async Task Should_Not_Throw_When_Disposing_Session_After_Stopping_Client()
221240
{

dotnet/test/CloneTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public void CopilotClientOptions_Clone_CopiesAllProperties()
2626
Environment = new Dictionary<string, string> { ["KEY"] = "value" },
2727
GitHubToken = "ghp_test",
2828
UseLoggedInUser = false,
29+
SessionIdleTimeoutSeconds = 600,
2930
};
3031

3132
var clone = original.Clone();
@@ -42,6 +43,7 @@ public void CopilotClientOptions_Clone_CopiesAllProperties()
4243
Assert.Equal(original.Environment, clone.Environment);
4344
Assert.Equal(original.GitHubToken, clone.GitHubToken);
4445
Assert.Equal(original.UseLoggedInUser, clone.UseLoggedInUser);
46+
Assert.Equal(original.SessionIdleTimeoutSeconds, clone.SessionIdleTimeoutSeconds);
4547
}
4648

4749
[Fact]

go/client.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,10 @@ func NewClient(options *ClientOptions) *Client {
215215
sessionFs := *options.SessionFs
216216
opts.SessionFs = &sessionFs
217217
}
218+
if options.Telemetry != nil {
219+
opts.Telemetry = options.Telemetry
220+
}
221+
opts.SessionIdleTimeoutSeconds = options.SessionIdleTimeoutSeconds
218222
}
219223

220224
// Default Env to current environment if not set
@@ -1378,6 +1382,10 @@ func (c *Client) startCLIServer(ctx context.Context) error {
13781382
args = append(args, "--no-auto-login")
13791383
}
13801384

1385+
if c.options.SessionIdleTimeoutSeconds > 0 {
1386+
args = append(args, "--session-idle-timeout", strconv.Itoa(c.options.SessionIdleTimeoutSeconds))
1387+
}
1388+
13811389
// If CLIPath is a .js file, run it with node
13821390
// Note we can't rely on the shebang as Windows doesn't support it
13831391
command := cliPath

go/client_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,26 @@ func TestClient_EnvOptions(t *testing.T) {
391391
})
392392
}
393393

394+
func TestClient_SessionIdleTimeoutSeconds(t *testing.T) {
395+
t.Run("should store SessionIdleTimeoutSeconds option", func(t *testing.T) {
396+
client := NewClient(&ClientOptions{
397+
SessionIdleTimeoutSeconds: 600,
398+
})
399+
400+
if client.options.SessionIdleTimeoutSeconds != 600 {
401+
t.Errorf("Expected SessionIdleTimeoutSeconds to be 600, got %d", client.options.SessionIdleTimeoutSeconds)
402+
}
403+
})
404+
405+
t.Run("should default SessionIdleTimeoutSeconds to zero", func(t *testing.T) {
406+
client := NewClient(&ClientOptions{})
407+
408+
if client.options.SessionIdleTimeoutSeconds != 0 {
409+
t.Errorf("Expected SessionIdleTimeoutSeconds to be 0, got %d", client.options.SessionIdleTimeoutSeconds)
410+
}
411+
})
412+
}
413+
394414
func findCLIPathForTest() string {
395415
abs, _ := filepath.Abs("../nodejs/node_modules/@github/copilot/index.js")
396416
if fileExistsForTest(abs) {

go/types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ type ClientOptions struct {
7171
// When non-nil, COPILOT_OTEL_ENABLED=true is set and any populated fields
7272
// are mapped to the corresponding environment variables.
7373
Telemetry *TelemetryConfig
74+
// SessionIdleTimeoutSeconds configures the server-wide session idle timeout in seconds.
75+
// Sessions without activity for this duration are automatically cleaned up.
76+
// Set to 0 or leave unset to disable (sessions live indefinitely).
77+
// This option is only used when the SDK spawns the CLI process; it is ignored
78+
// when connecting to an external server via CLIUrl.
79+
SessionIdleTimeoutSeconds int
7480
}
7581

7682
// TelemetryConfig configures OpenTelemetry integration for the Copilot CLI process.

nodejs/src/client.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ export class CopilotClient {
340340
// Default useLoggedInUser to false when githubToken is provided, otherwise true
341341
useLoggedInUser: options.useLoggedInUser ?? (options.githubToken ? false : true),
342342
telemetry: options.telemetry,
343+
sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0,
343344
};
344345
}
345346

@@ -1414,6 +1415,16 @@ export class CopilotClient {
14141415
args.push("--no-auto-login");
14151416
}
14161417

1418+
if (
1419+
this.options.sessionIdleTimeoutSeconds !== undefined &&
1420+
this.options.sessionIdleTimeoutSeconds > 0
1421+
) {
1422+
args.push(
1423+
"--session-idle-timeout",
1424+
this.options.sessionIdleTimeoutSeconds.toString()
1425+
);
1426+
}
1427+
14171428
// Suppress debug/trace output that might pollute stdout
14181429
const envWithoutNodeDebug = { ...this.options.env };
14191430
delete envWithoutNodeDebug.NODE_DEBUG;

nodejs/src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,16 @@ export interface CopilotClientOptions {
184184
* instead of the server's default local filesystem storage.
185185
*/
186186
sessionFs?: SessionFsConfig;
187+
188+
/**
189+
* Server-wide idle timeout for sessions in seconds.
190+
* Sessions without activity for this duration are automatically cleaned up.
191+
* Set to 0 or omit to disable (sessions live indefinitely).
192+
* This option is only used when the SDK spawns the CLI process; it is ignored
193+
* when connecting to an external server via {@link cliUrl}.
194+
* @default undefined (disabled)
195+
*/
196+
sessionIdleTimeoutSeconds?: number;
187197
}
188198

189199
/**

0 commit comments

Comments
 (0)