Skip to content

Commit 5a41532

Browse files
Remove autoRestart feature across all SDKs (#803)
* Remove autoRestart feature across all SDKs The autoRestart option never worked correctly. This removes it from: - Node.js: types, client options, reconnect logic - Python: types, client options - Go: types, client options, struct field - .NET: types, clone copy, tests - Docs: setup, troubleshooting, READMEs - Agent config: docs-maintenance validation lists Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Mark client as disconnected on connection close/error Instead of leaving onClose/onError as no-ops (which would leave the client in a stale 'connected' state), transition to 'disconnected' so callers fail fast or can re-start cleanly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix numbered list after removing auto-restart step Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Transition to disconnected state on unexpected process/connection death All SDKs now properly transition their connection state to 'disconnected' when the child process exits unexpectedly or the TCP connection drops: - Node.js: onClose/onError handlers in attachConnectionHandlers() - Go: onClose callback fired from readLoop() on unexpected exit - Python: on_close callback fired from _read_loop() on unexpected exit - .NET: rpc.Completion continuation sets _disconnected flag Includes unit tests for all four SDKs verifying the state transition. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove .NET disconnection test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Re-add autoRestart as deprecated no-op to avoid source-breaking change Mark the option as obsolete/deprecated in Go, .NET, and TypeScript so existing consumers continue to compile without changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix go fmt alignment in Client struct Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Go onClose deadlock by running state update in goroutine The onClose callback acquires startStopMux, but Stop/ForceStop already hold that lock while waiting for readLoop to finish via wg.Wait(). Running the state update in a goroutine allows readLoop to complete, breaking the circular wait. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent df59a0e commit 5a41532

File tree

21 files changed

+225
-72
lines changed

21 files changed

+225
-72
lines changed

.github/agents/docs-maintenance.agent.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@ Every major SDK feature should be documented. Core features include:
122122
- Client initialization and configuration
123123
- Connection modes (stdio vs TCP)
124124
- Authentication options
125-
- Auto-start and auto-restart behavior
126125

127126
**Session Management:**
128127
- Creating sessions
@@ -342,7 +341,7 @@ cat nodejs/src/types.ts | grep -A 10 "export interface ExportSessionOptions"
342341
```
343342

344343
**Must match:**
345-
- `CopilotClient` constructor options: `cliPath`, `cliUrl`, `useStdio`, `port`, `logLevel`, `autoStart`, `autoRestart`, `env`, `githubToken`, `useLoggedInUser`
344+
- `CopilotClient` constructor options: `cliPath`, `cliUrl`, `useStdio`, `port`, `logLevel`, `autoStart`, `env`, `githubToken`, `useLoggedInUser`
346345
- `createSession()` config: `model`, `tools`, `hooks`, `systemMessage`, `mcpServers`, `availableTools`, `excludedTools`, `streaming`, `reasoningEffort`, `provider`, `infiniteSessions`, `customAgents`, `workingDirectory`
347346
- `CopilotSession` methods: `send()`, `sendAndWait()`, `getMessages()`, `disconnect()`, `abort()`, `on()`, `once()`, `off()`
348347
- Hook names: `onPreToolUse`, `onPostToolUse`, `onUserPromptSubmitted`, `onSessionStart`, `onSessionEnd`, `onErrorOccurred`
@@ -360,7 +359,7 @@ cat python/copilot/types.py | grep -A 15 "class SessionHooks"
360359
```
361360

362361
**Must match (snake_case):**
363-
- `CopilotClient` options: `cli_path`, `cli_url`, `use_stdio`, `port`, `log_level`, `auto_start`, `auto_restart`, `env`, `github_token`, `use_logged_in_user`
362+
- `CopilotClient` options: `cli_path`, `cli_url`, `use_stdio`, `port`, `log_level`, `auto_start`, `env`, `github_token`, `use_logged_in_user`
364363
- `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`
365364
- `CopilotSession` methods: `send()`, `send_and_wait()`, `get_messages()`, `disconnect()`, `abort()`, `export_session()`
366365
- Hook names: `on_pre_tool_use`, `on_post_tool_use`, `on_user_prompt_submitted`, `on_session_start`, `on_session_end`, `on_error_occurred`
@@ -378,7 +377,7 @@ cat go/types.go | grep -A 15 "type SessionHooks struct"
378377
```
379378

380379
**Must match (PascalCase for exported):**
381-
- `ClientOptions` fields: `CLIPath`, `CLIUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `AutoRestart`, `Env`, `GithubToken`, `UseLoggedInUser`
380+
- `ClientOptions` fields: `CLIPath`, `CLIUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `Env`, `GithubToken`, `UseLoggedInUser`
382381
- `SessionConfig` fields: `Model`, `Tools`, `Hooks`, `SystemMessage`, `MCPServers`, `AvailableTools`, `ExcludedTools`, `Streaming`, `ReasoningEffort`, `Provider`, `InfiniteSessions`, `CustomAgents`, `WorkingDirectory`
383382
- `Session` methods: `Send()`, `SendAndWait()`, `GetMessages()`, `Disconnect()`, `Abort()`, `ExportSession()`
384383
- Hook fields: `OnPreToolUse`, `OnPostToolUse`, `OnUserPromptSubmitted`, `OnSessionStart`, `OnSessionEnd`, `OnErrorOccurred`
@@ -396,7 +395,7 @@ cat dotnet/src/Types.cs | grep -A 15 "public class SessionHooks"
396395
```
397396

398397
**Must match (PascalCase):**
399-
- `CopilotClientOptions` properties: `CliPath`, `CliUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `AutoRestart`, `Environment`, `GithubToken`, `UseLoggedInUser`
398+
- `CopilotClientOptions` properties: `CliPath`, `CliUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `Environment`, `GithubToken`, `UseLoggedInUser`
400399
- `SessionConfig` properties: `Model`, `Tools`, `Hooks`, `SystemMessage`, `McpServers`, `AvailableTools`, `ExcludedTools`, `Streaming`, `ReasoningEffort`, `Provider`, `InfiniteSessions`, `CustomAgents`, `WorkingDirectory`
401400
- `CopilotSession` methods: `SendAsync()`, `SendAndWaitAsync()`, `GetMessagesAsync()`, `DisposeAsync()`, `AbortAsync()`, `ExportSessionAsync()`
402401
- Hook properties: `OnPreToolUse`, `OnPostToolUse`, `OnUserPromptSubmitted`, `OnSessionStart`, `OnSessionEnd`, `OnErrorOccurred`

docs/setup/local-cli.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,6 @@ const client = new CopilotClient({
171171

172172
// Set working directory
173173
cwd: "/path/to/project",
174-
175-
// Auto-restart CLI if it crashes (default: true)
176-
autoRestart: true,
177174
});
178175
```
179176

docs/troubleshooting/debugging.md

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -297,14 +297,7 @@ var client = new CopilotClient(new CopilotClientOptions
297297
copilot --server --stdio
298298
```
299299

300-
2. Enable auto-restart (enabled by default):
301-
```typescript
302-
const client = new CopilotClient({
303-
autoRestart: true,
304-
});
305-
```
306-
307-
3. Check for port conflicts if using TCP mode:
300+
2. Check for port conflicts if using TCP mode:
308301
```typescript
309302
const client = new CopilotClient({
310303
useStdio: false,

dotnet/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ new CopilotClient(CopilotClientOptions? options = null)
7373
- `UseStdio` - Use stdio transport instead of TCP (default: true)
7474
- `LogLevel` - Log level (default: "info")
7575
- `AutoStart` - Auto-start server (default: true)
76-
- `AutoRestart` - Auto-restart on crash (default: true)
7776
- `Cwd` - Working directory for the CLI process
7877
- `Environment` - Environment variables to pass to the CLI process
7978
- `Logger` - `ILogger` instance for SDK logging

dotnet/src/Client.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
6666
private readonly CopilotClientOptions _options;
6767
private readonly ILogger _logger;
6868
private Task<Connection>? _connectionTask;
69+
private volatile bool _disconnected;
6970
private bool _disposed;
7071
private readonly int? _optionsPort;
7172
private readonly string? _optionsHost;
@@ -202,6 +203,7 @@ public Task StartAsync(CancellationToken cancellationToken = default)
202203
async Task<Connection> StartCoreAsync(CancellationToken ct)
203204
{
204205
_logger.LogDebug("Starting Copilot client");
206+
_disconnected = false;
205207

206208
Task<Connection> result;
207209

@@ -593,6 +595,7 @@ public ConnectionState State
593595
if (_connectionTask == null) return ConnectionState.Disconnected;
594596
if (_connectionTask.IsFaulted) return ConnectionState.Error;
595597
if (!_connectionTask.IsCompleted) return ConnectionState.Connecting;
598+
if (_disconnected) return ConnectionState.Disconnected;
596599
return ConnectionState.Connected;
597600
}
598601
}
@@ -1201,6 +1204,9 @@ private async Task<Connection> ConnectToServerAsync(Process? cliProcess, string?
12011204
rpc.AddLocalRpcMethod("hooks.invoke", handler.OnHooksInvoke);
12021205
rpc.StartListening();
12031206

1207+
// Transition state to Disconnected if the JSON-RPC connection drops
1208+
_ = rpc.Completion.ContinueWith(_ => _disconnected = true, TaskScheduler.Default);
1209+
12041210
_rpc = new ServerRpc(rpc);
12051211

12061212
return new Connection(rpc, cliProcess, tcpClient, networkStream, stderrBuffer);

dotnet/src/Types.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ protected CopilotClientOptions(CopilotClientOptions? other)
5050
{
5151
if (other is null) return;
5252

53-
AutoRestart = other.AutoRestart;
5453
AutoStart = other.AutoStart;
54+
#pragma warning disable CS0618 // Obsolete member
55+
AutoRestart = other.AutoRestart;
56+
#pragma warning restore CS0618
5557
CliArgs = (string[]?)other.CliArgs?.Clone();
5658
CliPath = other.CliPath;
5759
CliUrl = other.CliUrl;
@@ -99,9 +101,10 @@ protected CopilotClientOptions(CopilotClientOptions? other)
99101
/// </summary>
100102
public bool AutoStart { get; set; } = true;
101103
/// <summary>
102-
/// Whether to automatically restart the CLI server if it exits unexpectedly.
104+
/// Obsolete. This option has no effect.
103105
/// </summary>
104-
public bool AutoRestart { get; set; } = true;
106+
[Obsolete("AutoRestart has no effect and will be removed in a future release.")]
107+
public bool AutoRestart { get; set; }
105108
/// <summary>
106109
/// Environment variables to pass to the CLI process.
107110
/// </summary>

dotnet/test/CloneTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public void CopilotClientOptions_Clone_CopiesAllProperties()
2222
CliUrl = "http://localhost:8080",
2323
LogLevel = "debug",
2424
AutoStart = false,
25-
AutoRestart = false,
25+
2626
Environment = new Dictionary<string, string> { ["KEY"] = "value" },
2727
GitHubToken = "ghp_test",
2828
UseLoggedInUser = false,
@@ -38,7 +38,7 @@ public void CopilotClientOptions_Clone_CopiesAllProperties()
3838
Assert.Equal(original.CliUrl, clone.CliUrl);
3939
Assert.Equal(original.LogLevel, clone.LogLevel);
4040
Assert.Equal(original.AutoStart, clone.AutoStart);
41-
Assert.Equal(original.AutoRestart, clone.AutoRestart);
41+
4242
Assert.Equal(original.Environment, clone.Environment);
4343
Assert.Equal(original.GitHubToken, clone.GitHubToken);
4444
Assert.Equal(original.UseLoggedInUser, clone.UseLoggedInUser);

go/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec
138138
- `UseStdio` (bool): Use stdio transport instead of TCP (default: true)
139139
- `LogLevel` (string): Log level (default: "info")
140140
- `AutoStart` (\*bool): Auto-start server on first use (default: true). Use `Bool(false)` to disable.
141-
- `AutoRestart` (\*bool): Auto-restart on crash (default: true). Use `Bool(false)` to disable.
142141
- `Env` ([]string): Environment variables for CLI process (default: inherits from current process)
143142
- `GitHubToken` (string): GitHub token for authentication. When provided, takes priority over other auth methods.
144143
- `UseLoggedInUser` (\*bool): Whether to use logged-in user for authentication (default: true, but false when `GitHubToken` is provided). Cannot be used with `CLIUrl`.
@@ -174,7 +173,7 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec
174173

175174
### Helper Functions
176175

177-
- `Bool(v bool) *bool` - Helper to create bool pointers for `AutoStart`/`AutoRestart` options
176+
- `Bool(v bool) *bool` - Helper to create bool pointers for `AutoStart` option
178177

179178
## Image Support
180179

go/client.go

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -73,19 +73,19 @@ const noResultPermissionV2Error = "permission handlers cannot return 'no-result'
7373
// }
7474
// defer client.Stop()
7575
type Client struct {
76-
options ClientOptions
77-
process *exec.Cmd
78-
client *jsonrpc2.Client
79-
actualPort int
80-
actualHost string
81-
state ConnectionState
82-
sessions map[string]*Session
83-
sessionsMux sync.Mutex
84-
isExternalServer bool
85-
conn net.Conn // stores net.Conn for external TCP connections
86-
useStdio bool // resolved value from options
87-
autoStart bool // resolved value from options
88-
autoRestart bool // resolved value from options
76+
options ClientOptions
77+
process *exec.Cmd
78+
client *jsonrpc2.Client
79+
actualPort int
80+
actualHost string
81+
state ConnectionState
82+
sessions map[string]*Session
83+
sessionsMux sync.Mutex
84+
isExternalServer bool
85+
conn net.Conn // stores net.Conn for external TCP connections
86+
useStdio bool // resolved value from options
87+
autoStart bool // resolved value from options
88+
8989
modelsCache []ModelInfo
9090
modelsCacheMux sync.Mutex
9191
lifecycleHandlers []SessionLifecycleHandler
@@ -134,7 +134,6 @@ func NewClient(options *ClientOptions) *Client {
134134
isExternalServer: false,
135135
useStdio: true,
136136
autoStart: true, // default
137-
autoRestart: true, // default
138137
}
139138

140139
if options != nil {
@@ -184,9 +183,6 @@ func NewClient(options *ClientOptions) *Client {
184183
if options.AutoStart != nil {
185184
client.autoStart = *options.AutoStart
186185
}
187-
if options.AutoRestart != nil {
188-
client.autoRestart = *options.AutoRestart
189-
}
190186
if options.GitHubToken != "" {
191187
opts.GitHubToken = options.GitHubToken
192188
}
@@ -1233,6 +1229,15 @@ func (c *Client) startCLIServer(ctx context.Context) error {
12331229
// Create JSON-RPC client immediately
12341230
c.client = jsonrpc2.NewClient(stdin, stdout)
12351231
c.client.SetProcessDone(c.processDone, c.processErrorPtr)
1232+
c.client.SetOnClose(func() {
1233+
// Run in a goroutine to avoid deadlocking with Stop/ForceStop,
1234+
// which hold startStopMux while waiting for readLoop to finish.
1235+
go func() {
1236+
c.startStopMux.Lock()
1237+
defer c.startStopMux.Unlock()
1238+
c.state = StateDisconnected
1239+
}()
1240+
})
12361241
c.RPC = rpc.NewServerRpc(c.client)
12371242
c.setupNotificationHandler()
12381243
c.client.Start()
@@ -1348,6 +1353,13 @@ func (c *Client) connectViaTcp(ctx context.Context) error {
13481353
if c.processDone != nil {
13491354
c.client.SetProcessDone(c.processDone, c.processErrorPtr)
13501355
}
1356+
c.client.SetOnClose(func() {
1357+
go func() {
1358+
c.startStopMux.Lock()
1359+
defer c.startStopMux.Unlock()
1360+
c.state = StateDisconnected
1361+
}()
1362+
})
13511363
c.RPC = rpc.NewServerRpc(c.client)
13521364
c.setupNotificationHandler()
13531365
c.client.Start()

go/internal/jsonrpc2/jsonrpc2.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ type Client struct {
6161
processDone chan struct{} // closed when the underlying process exits
6262
processError error // set before processDone is closed
6363
processErrorMu sync.RWMutex // protects processError
64+
onClose func() // called when the read loop exits unexpectedly
6465
}
6566

6667
// NewClient creates a new JSON-RPC client
@@ -293,9 +294,22 @@ func (c *Client) sendMessage(message any) error {
293294
return nil
294295
}
295296

297+
// SetOnClose sets a callback invoked when the read loop exits unexpectedly
298+
// (e.g. the underlying connection or process was lost).
299+
func (c *Client) SetOnClose(fn func()) {
300+
c.onClose = fn
301+
}
302+
296303
// readLoop reads messages from stdout in a background goroutine
297304
func (c *Client) readLoop() {
298305
defer c.wg.Done()
306+
defer func() {
307+
// If still running, the read loop exited unexpectedly (process died or
308+
// connection dropped). Notify the caller so it can update its state.
309+
if c.onClose != nil && c.running.Load() {
310+
c.onClose()
311+
}
312+
}()
299313

300314
reader := bufio.NewReader(c.stdout)
301315

0 commit comments

Comments
 (0)