Skip to content
Merged
9 changes: 4 additions & 5 deletions .github/agents/docs-maintenance.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ Every major SDK feature should be documented. Core features include:
- Client initialization and configuration
- Connection modes (stdio vs TCP)
- Authentication options
- Auto-start and auto-restart behavior

**Session Management:**
- Creating sessions
Expand Down Expand Up @@ -342,7 +341,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`
- `CopilotClient` constructor options: `cliPath`, `cliUrl`, `useStdio`, `port`, `logLevel`, `autoStart`, `env`, `githubToken`, `useLoggedInUser`
- `createSession()` config: `model`, `tools`, `hooks`, `systemMessage`, `mcpServers`, `availableTools`, `excludedTools`, `streaming`, `reasoningEffort`, `provider`, `infiniteSessions`, `customAgents`, `workingDirectory`
- `CopilotSession` methods: `send()`, `sendAndWait()`, `getMessages()`, `disconnect()`, `abort()`, `on()`, `once()`, `off()`
- Hook names: `onPreToolUse`, `onPostToolUse`, `onUserPromptSubmitted`, `onSessionStart`, `onSessionEnd`, `onErrorOccurred`
Expand All @@ -360,7 +359,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`
- `CopilotClient` options: `cli_path`, `cli_url`, `use_stdio`, `port`, `log_level`, `auto_start`, `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()`, `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`
Expand All @@ -378,7 +377,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`
- `ClientOptions` fields: `CLIPath`, `CLIUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `Env`, `GithubToken`, `UseLoggedInUser`
- `SessionConfig` fields: `Model`, `Tools`, `Hooks`, `SystemMessage`, `MCPServers`, `AvailableTools`, `ExcludedTools`, `Streaming`, `ReasoningEffort`, `Provider`, `InfiniteSessions`, `CustomAgents`, `WorkingDirectory`
- `Session` methods: `Send()`, `SendAndWait()`, `GetMessages()`, `Disconnect()`, `Abort()`, `ExportSession()`
- Hook fields: `OnPreToolUse`, `OnPostToolUse`, `OnUserPromptSubmitted`, `OnSessionStart`, `OnSessionEnd`, `OnErrorOccurred`
Expand All @@ -396,7 +395,7 @@ cat dotnet/src/Types.cs | grep -A 15 "public class SessionHooks"
```

**Must match (PascalCase):**
- `CopilotClientOptions` properties: `CliPath`, `CliUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `AutoRestart`, `Environment`, `GithubToken`, `UseLoggedInUser`
- `CopilotClientOptions` properties: `CliPath`, `CliUrl`, `UseStdio`, `Port`, `LogLevel`, `AutoStart`, `Environment`, `GithubToken`, `UseLoggedInUser`
- `SessionConfig` properties: `Model`, `Tools`, `Hooks`, `SystemMessage`, `McpServers`, `AvailableTools`, `ExcludedTools`, `Streaming`, `ReasoningEffort`, `Provider`, `InfiniteSessions`, `CustomAgents`, `WorkingDirectory`
- `CopilotSession` methods: `SendAsync()`, `SendAndWaitAsync()`, `GetMessagesAsync()`, `DisposeAsync()`, `AbortAsync()`, `ExportSessionAsync()`
- Hook properties: `OnPreToolUse`, `OnPostToolUse`, `OnUserPromptSubmitted`, `OnSessionStart`, `OnSessionEnd`, `OnErrorOccurred`
Expand Down
3 changes: 0 additions & 3 deletions docs/setup/local-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,6 @@ const client = new CopilotClient({

// Set working directory
cwd: "/path/to/project",

// Auto-restart CLI if it crashes (default: true)
autoRestart: true,
});
```

Expand Down
9 changes: 1 addition & 8 deletions docs/troubleshooting/debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,14 +297,7 @@ var client = new CopilotClient(new CopilotClientOptions
copilot --server --stdio
```

2. Enable auto-restart (enabled by default):
```typescript
const client = new CopilotClient({
autoRestart: true,
});
```

3. Check for port conflicts if using TCP mode:
2. Check for port conflicts if using TCP mode:
```typescript
const client = new CopilotClient({
useStdio: false,
Expand Down
1 change: 0 additions & 1 deletion dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ new CopilotClient(CopilotClientOptions? options = null)
- `UseStdio` - Use stdio transport instead of TCP (default: true)
- `LogLevel` - Log level (default: "info")
- `AutoStart` - Auto-start server (default: true)
- `AutoRestart` - Auto-restart on crash (default: true)
- `Cwd` - Working directory for the CLI process
- `Environment` - Environment variables to pass to the CLI process
- `Logger` - `ILogger` instance for SDK logging
Expand Down
6 changes: 6 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public sealed partial class CopilotClient : IDisposable, IAsyncDisposable
private readonly CopilotClientOptions _options;
private readonly ILogger _logger;
private Task<Connection>? _connectionTask;
private volatile bool _disconnected;
private bool _disposed;
private readonly int? _optionsPort;
private readonly string? _optionsHost;
Expand Down Expand Up @@ -199,6 +200,7 @@ public Task StartAsync(CancellationToken cancellationToken = default)
async Task<Connection> StartCoreAsync(CancellationToken ct)
{
_logger.LogDebug("Starting Copilot client");
_disconnected = false;

Task<Connection> result;

Expand Down Expand Up @@ -590,6 +592,7 @@ public ConnectionState State
if (_connectionTask == null) return ConnectionState.Disconnected;
if (_connectionTask.IsFaulted) return ConnectionState.Error;
if (!_connectionTask.IsCompleted) return ConnectionState.Connecting;
if (_disconnected) return ConnectionState.Disconnected;
return ConnectionState.Connected;
}
}
Expand Down Expand Up @@ -1198,6 +1201,9 @@ private async Task<Connection> ConnectToServerAsync(Process? cliProcess, string?
rpc.AddLocalRpcMethod("hooks.invoke", handler.OnHooksInvoke);
rpc.StartListening();

// Transition state to Disconnected if the JSON-RPC connection drops
_ = rpc.Completion.ContinueWith(_ => _disconnected = true, TaskScheduler.Default);

_rpc = new ServerRpc(rpc);

return new Connection(rpc, cliProcess, tcpClient, networkStream, stderrBuffer);
Expand Down
9 changes: 6 additions & 3 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ protected CopilotClientOptions(CopilotClientOptions? other)
{
if (other is null) return;

AutoRestart = other.AutoRestart;
AutoStart = other.AutoStart;
#pragma warning disable CS0618 // Obsolete member
AutoRestart = other.AutoRestart;
#pragma warning restore CS0618
CliArgs = (string[]?)other.CliArgs?.Clone();
CliPath = other.CliPath;
CliUrl = other.CliUrl;
Expand Down Expand Up @@ -99,9 +101,10 @@ protected CopilotClientOptions(CopilotClientOptions? other)
/// </summary>
public bool AutoStart { get; set; } = true;
/// <summary>
/// Whether to automatically restart the CLI server if it exits unexpectedly.
/// Obsolete. This option has no effect.
/// </summary>
public bool AutoRestart { get; set; } = true;
[Obsolete("AutoRestart has no effect and will be removed in a future release.")]
public bool AutoRestart { get; set; }
/// <summary>
/// Environment variables to pass to the CLI process.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions dotnet/test/CloneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public void CopilotClientOptions_Clone_CopiesAllProperties()
CliUrl = "http://localhost:8080",
LogLevel = "debug",
AutoStart = false,
AutoRestart = false,

Environment = new Dictionary<string, string> { ["KEY"] = "value" },
GitHubToken = "ghp_test",
UseLoggedInUser = false,
Expand All @@ -38,7 +38,7 @@ public void CopilotClientOptions_Clone_CopiesAllProperties()
Assert.Equal(original.CliUrl, clone.CliUrl);
Assert.Equal(original.LogLevel, clone.LogLevel);
Assert.Equal(original.AutoStart, clone.AutoStart);
Assert.Equal(original.AutoRestart, clone.AutoRestart);

Assert.Equal(original.Environment, clone.Environment);
Assert.Equal(original.GitHubToken, clone.GitHubToken);
Assert.Equal(original.UseLoggedInUser, clone.UseLoggedInUser);
Expand Down
3 changes: 1 addition & 2 deletions go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec
- `UseStdio` (bool): Use stdio transport instead of TCP (default: true)
- `LogLevel` (string): Log level (default: "info")
- `AutoStart` (\*bool): Auto-start server on first use (default: true). Use `Bool(false)` to disable.
- `AutoRestart` (\*bool): Auto-restart on crash (default: true). Use `Bool(false)` to disable.
- `Env` ([]string): Environment variables for CLI process (default: inherits from current process)
- `GitHubToken` (string): GitHub token for authentication. When provided, takes priority over other auth methods.
- `UseLoggedInUser` (\*bool): Whether to use logged-in user for authentication (default: true, but false when `GitHubToken` is provided). Cannot be used with `CLIUrl`.
Expand Down Expand Up @@ -174,7 +173,7 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec

### Helper Functions

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

## Image Support

Expand Down
46 changes: 29 additions & 17 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,19 @@ import (
// }
// defer client.Stop()
type Client struct {
options ClientOptions
process *exec.Cmd
client *jsonrpc2.Client
actualPort int
actualHost string
state ConnectionState
sessions map[string]*Session
sessionsMux sync.Mutex
isExternalServer bool
conn net.Conn // stores net.Conn for external TCP connections
useStdio bool // resolved value from options
autoStart bool // resolved value from options
autoRestart bool // resolved value from options
options ClientOptions
process *exec.Cmd
client *jsonrpc2.Client
actualPort int
actualHost string
state ConnectionState
sessions map[string]*Session
sessionsMux sync.Mutex
isExternalServer bool
conn net.Conn // stores net.Conn for external TCP connections
useStdio bool // resolved value from options
autoStart bool // resolved value from options

modelsCache []ModelInfo
modelsCacheMux sync.Mutex
lifecycleHandlers []SessionLifecycleHandler
Expand Down Expand Up @@ -132,7 +132,6 @@ func NewClient(options *ClientOptions) *Client {
isExternalServer: false,
useStdio: true,
autoStart: true, // default
autoRestart: true, // default
}

if options != nil {
Expand Down Expand Up @@ -182,9 +181,6 @@ func NewClient(options *ClientOptions) *Client {
if options.AutoStart != nil {
client.autoStart = *options.AutoStart
}
if options.AutoRestart != nil {
client.autoRestart = *options.AutoRestart
}
if options.GitHubToken != "" {
opts.GitHubToken = options.GitHubToken
}
Expand Down Expand Up @@ -1231,6 +1227,15 @@ func (c *Client) startCLIServer(ctx context.Context) error {
// Create JSON-RPC client immediately
c.client = jsonrpc2.NewClient(stdin, stdout)
c.client.SetProcessDone(c.processDone, c.processErrorPtr)
c.client.SetOnClose(func() {
// Run in a goroutine to avoid deadlocking with Stop/ForceStop,
// which hold startStopMux while waiting for readLoop to finish.
go func() {
c.startStopMux.Lock()
defer c.startStopMux.Unlock()
c.state = StateDisconnected
}()
})
c.RPC = rpc.NewServerRpc(c.client)
c.setupNotificationHandler()
c.client.Start()
Expand Down Expand Up @@ -1346,6 +1351,13 @@ func (c *Client) connectViaTcp(ctx context.Context) error {
if c.processDone != nil {
c.client.SetProcessDone(c.processDone, c.processErrorPtr)
}
c.client.SetOnClose(func() {
go func() {
c.startStopMux.Lock()
defer c.startStopMux.Unlock()
c.state = StateDisconnected
}()
})
c.RPC = rpc.NewServerRpc(c.client)
c.setupNotificationHandler()
c.client.Start()
Expand Down
14 changes: 14 additions & 0 deletions go/internal/jsonrpc2/jsonrpc2.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type Client struct {
processDone chan struct{} // closed when the underlying process exits
processError error // set before processDone is closed
processErrorMu sync.RWMutex // protects processError
onClose func() // called when the read loop exits unexpectedly
}

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

// SetOnClose sets a callback invoked when the read loop exits unexpectedly
// (e.g. the underlying connection or process was lost).
func (c *Client) SetOnClose(fn func()) {
c.onClose = fn
}

// readLoop reads messages from stdout in a background goroutine
func (c *Client) readLoop() {
defer c.wg.Done()
defer func() {
// If still running, the read loop exited unexpectedly (process died or
// connection dropped). Notify the caller so it can update its state.
if c.onClose != nil && c.running.Load() {
c.onClose()
}
}()

reader := bufio.NewReader(c.stdout)

Expand Down
69 changes: 69 additions & 0 deletions go/internal/jsonrpc2/jsonrpc2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package jsonrpc2

import (
"io"
"sync"
"testing"
"time"
)

func TestOnCloseCalledOnUnexpectedExit(t *testing.T) {
stdinR, stdinW := io.Pipe()
stdoutR, stdoutW := io.Pipe()
defer stdinR.Close()

client := NewClient(stdinW, stdoutR)

var called bool
var mu sync.Mutex
client.SetOnClose(func() {
mu.Lock()
called = true
mu.Unlock()
})

client.Start()

// Simulate unexpected process death by closing the stdout writer
stdoutW.Close()

// Wait for readLoop to detect the close and invoke the callback
time.Sleep(200 * time.Millisecond)

mu.Lock()
defer mu.Unlock()
if !called {
t.Error("expected onClose to be called when read loop exits unexpectedly")
}
}

func TestOnCloseNotCalledOnIntentionalStop(t *testing.T) {
stdinR, stdinW := io.Pipe()
stdoutR, stdoutW := io.Pipe()
defer stdinR.Close()
defer stdoutW.Close()

client := NewClient(stdinW, stdoutR)

var called bool
var mu sync.Mutex
client.SetOnClose(func() {
mu.Lock()
called = true
mu.Unlock()
})

client.Start()

// Intentional stop — should set running=false before closing stdout,
// so the readLoop should NOT invoke onClose.
client.Stop()

time.Sleep(200 * time.Millisecond)

mu.Lock()
defer mu.Unlock()
if called {
t.Error("onClose should not be called on intentional Stop()")
}
}
5 changes: 2 additions & 3 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ type ClientOptions struct {
// AutoStart automatically starts the CLI server on first use (default: true).
// Use Bool(false) to disable.
AutoStart *bool
// AutoRestart automatically restarts the CLI server if it crashes (default: true).
// Use Bool(false) to disable.
// Deprecated: AutoRestart has no effect and will be removed in a future release.
AutoRestart *bool
// Env is the environment variables for the CLI process (default: inherits from current process).
// Each entry is of the form "key=value".
Expand All @@ -65,7 +64,7 @@ type ClientOptions struct {
}

// Bool returns a pointer to the given bool value.
// Use for setting AutoStart or AutoRestart: AutoStart: Bool(false)
// Use for setting AutoStart: AutoStart: Bool(false)
func Bool(v bool) *bool {
return &v
}
Expand Down
1 change: 0 additions & 1 deletion nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ new CopilotClient(options?: CopilotClientOptions)
- `useStdio?: boolean` - Use stdio transport instead of TCP (default: true)
- `logLevel?: string` - Log level (default: "info")
- `autoStart?: boolean` - Auto-start server (default: true)
- `autoRestart?: boolean` - Auto-restart on crash (default: true)
- `githubToken?: string` - GitHub token for authentication. When provided, takes priority over other auth methods.
- `useLoggedInUser?: boolean` - Whether to use logged-in user for authentication (default: true, but false when `githubToken` is provided). Cannot be used with `cliUrl`.

Expand Down
Loading
Loading