github/copilot-sdk · error

failed to gracefully shut down runtime

Error message

failed to gracefully shut down runtime: %w

What it means

During CopilotClient.Stop, the SDK requests a graceful runtime shutdown and waits for a result. If the shutdown goroutine returns an error, Stop appends this wrapped error, indicating the runtime rejected or failed the shutdown request.

Solutions

  1. Read the wrapped cause for the runtime's shutdown error.
  2. Verify the runtime process is responsive (logs, health check).
  3. If shutdown repeatedly fails, force-kill the runtime process as a last resort.
  4. Upgrade SDK/runtime if the error indicates a known shutdown bug.

Example fix

// before
client.Stop(ctx) // ignore partial shutdown errors
// after
if err := client.Stop(ctx); err != nil {
    log.Printf("runtime shutdown issue: %v", err)
    // consider force-killing the runtime process
}
Defensive patterns

Strategy: try-catch

Try / catch

if err := client.Stop(ctx); err != nil { if strings.Contains(err.Error(), "gracefully shut down runtime") { /* check runtime process */ } }

Prevention

When it happens

Trigger: Calling Stop when the runtime.shutdown RPC returns an error (e.g. runtime in a bad state, request failed mid-flight).

Common situations: Runtime hung or wedged; JSON-RPC connection broken during shutdown; runtime binary crash during cleanup.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09). Data as JSON: /api/errors/c28b553b1c3d5a67. Report an issue: GitHub.

Appendix: source

Thrown at go/client.go:608

	c.clearGitHubTokenProviders()

	c.startStopMux.Lock()
	defer c.startStopMux.Unlock()

	if (c.process != nil || c.ffiHost != nil) && !c.isExternalServer && c.RPC != nil {
		rpcClient := c.RPC
		runtimeShutdownStart := time.Now()
		shutdownDone := make(chan error, 1)
		go func() {
			_, err := rpcClient.Runtime.Shutdown(context.Background())
			shutdownDone <- err
		}()

		select {
		case err := <-shutdownDone:
			if err != nil {
				c.logDebugTiming(runtimeShutdownStart, "CopilotClient.Stop runtime shutdown failed")
				errs = append(errs, fmt.Errorf("failed to gracefully shut down runtime: %w", err))
			} else {
				c.logDebugTiming(runtimeShutdownStart, "CopilotClient.Stop runtime shutdown complete")
			}
		case <-time.After(runtimeShutdownTimeout):
			c.logDebugTiming(runtimeShutdownStart, "CopilotClient.Stop runtime shutdown timed out")
			errs = append(errs, fmt.Errorf("timed out gracefully shutting down runtime after %s", runtimeShutdownTimeout))
		}
	}

	// The runtime completes all cleanup before responding to runtime.shutdown
	// and then leaves termination to us; it deliberately keeps its JSON-RPC
	// server alive to send the response and never self-exits. Waiting for a
	// self-exit that will never come just wastes time, so terminate the child
	// immediately and only wait to reap it.
	if c.process != nil && !c.isExternalServer {
		if err := c.killProcessAndWait(); err != nil {
			errs = append(errs, err)
		}

View on GitHub (pinned to cd8cf15dc3)