charmbracelet/crush · error · ErrClientRetired

client has been retired

Error message

client has been retired

What it means

ErrClientRetired reports that the client identifier used in a request has been retired by the server: its claims were released and the server will no longer accept operations under that ID. admitLocked rejects such requests, and handleError maps the condition for transport layers. The client must re-register under a fresh identity.

Source

Thrown at internal/backend/backend.go:42

	"github.com/charmbracelet/crush/internal/ui/util"
	"github.com/charmbracelet/crush/internal/version"
	"github.com/google/uuid"
)

// Common errors returned by backend operations.
var (
	ErrWorkspaceNotFound       = errors.New("workspace not found")
	ErrLSPClientNotFound       = errors.New("LSP client not found")
	ErrAgentNotInitialized     = errors.New("agent coordinator not initialized")
	ErrPathRequired            = errors.New("path is required")
	ErrInvalidPermissionAction = errors.New("invalid permission action")
	ErrUnknownCommand          = errors.New("unknown command")
	ErrInvalidClientID         = errors.New("invalid client_id")
	ErrClientNotAttached       = errors.New("client not attached")
	ErrWorkspaceClosing        = errors.New("workspace closing")
	ErrServerShuttingDown      = errors.New("server is shutting down")
	ErrServerNotIdle           = errors.New("server is hosting live workspaces")
	ErrClientRetired           = errors.New("client has been retired")
	ErrChannelOptInMismatch    = errors.New("requested channels differ from the existing workspace; channels are an explicit opt-in and are not shared across duplicate creates")
)

// DefaultCreateGrace is the window in which a client must open an SSE
// stream after creating a workspace before its creation hold is
// released. Exposed as a package variable so tests can shorten it.
var DefaultCreateGrace = 30 * time.Second

// DefaultIdleShutdownDelay is how long the server stays alive after its
// last workspace is released before it shuts itself down. The delay
// exists so a client that closes one session and opens another moments
// later (the same directory or a different one) reuses the still-running
// server instead of racing its shutdown: with an immediate shutdown the
// new client can attach to — or create a workspace on — a server that is
// already tearing down, and then observe its coder agent as "offline".
// Any workspace create within the window cancels the pending shutdown.
// Overridable via CRUSH_SERVER_IDLE_TIMEOUT (seconds; 0 restores the
// old shut-down-immediately behavior).

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Generate a new client_id and re-create/re-attach to the workspace
  2. On reconnect errors, re-attach promptly within the detach grace window
  3. Persist and reuse client_id only while the server process is known to be the same one

Example fix

// before
err := backend.SendMessage(ctx, retiredID, msg)
// after
if errors.Is(err, backend.ErrClientRetired) {
    clientID = uuid.NewString()
    err = backend.SendMessage(ctx, clientID, msg)
}
Defensive patterns

Strategy: fallback

Type guard

func isErrClientRetired(err error) bool {
    return errors.Is(err, backend.ErrClientRetired)
}

Try / catch

if err := backend.SendMessage(ctx, clientID, msg); err != nil {
    if errors.Is(err, backend.ErrClientRetired) {
        clientID = uuid.NewString() // fallback to fresh identity
        return backend.SendMessage(ctx, clientID, msg)
    }
    return err
}

Prevention

When it happens

Trigger: Any workspace-scoped call admitted through admitLocked after the server retired the client — typically because its SSE attachment dropped and the detach grace (DefaultDetachGrace) expired, or it explicitly released.

Common situations: A suspended laptop or network hiccup dropping the SSE stream, reconnect after longer than CRUSH_SERVER_DETACH_GRACE, reusing a serialized client_id from a previous run after the server restarted.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/ca383f632355763a. Report an issue: GitHub.