{"record":{"id":"ca383f632355763a","repo":"charmbracelet/crush","slug":"client-has-been-retired","errorCode":null,"errorMessage":"client has been retired","messagePattern":"client has been retired","errorType":"exception","errorClass":"ErrClientRetired","httpStatus":null,"severity":"error","filePath":"internal/backend/backend.go","lineNumber":42,"sourceCode":"\t\"github.com/charmbracelet/crush/internal/ui/util\"\n\t\"github.com/charmbracelet/crush/internal/version\"\n\t\"github.com/google/uuid\"\n)\n\n// Common errors returned by backend operations.\nvar (\n\tErrWorkspaceNotFound       = errors.New(\"workspace not found\")\n\tErrLSPClientNotFound       = errors.New(\"LSP client not found\")\n\tErrAgentNotInitialized     = errors.New(\"agent coordinator not initialized\")\n\tErrPathRequired            = errors.New(\"path is required\")\n\tErrInvalidPermissionAction = errors.New(\"invalid permission action\")\n\tErrUnknownCommand          = errors.New(\"unknown command\")\n\tErrInvalidClientID         = errors.New(\"invalid client_id\")\n\tErrClientNotAttached       = errors.New(\"client not attached\")\n\tErrWorkspaceClosing        = errors.New(\"workspace closing\")\n\tErrServerShuttingDown      = errors.New(\"server is shutting down\")\n\tErrServerNotIdle           = errors.New(\"server is hosting live workspaces\")\n\tErrClientRetired           = errors.New(\"client has been retired\")\n\tErrChannelOptInMismatch    = errors.New(\"requested channels differ from the existing workspace; channels are an explicit opt-in and are not shared across duplicate creates\")\n)\n\n// DefaultCreateGrace is the window in which a client must open an SSE\n// stream after creating a workspace before its creation hold is\n// released. Exposed as a package variable so tests can shorten it.\nvar DefaultCreateGrace = 30 * time.Second\n\n// DefaultIdleShutdownDelay is how long the server stays alive after its\n// last workspace is released before it shuts itself down. The delay\n// exists so a client that closes one session and opens another moments\n// later (the same directory or a different one) reuses the still-running\n// server instead of racing its shutdown: with an immediate shutdown the\n// new client can attach to — or create a workspace on — a server that is\n// already tearing down, and then observe its coder agent as \"offline\".\n// Any workspace create within the window cancels the pending shutdown.\n// Overridable via CRUSH_SERVER_IDLE_TIMEOUT (seconds; 0 restores the\n// old shut-down-immediately behavior).","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/backend/backend.go#L24-L60","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Generate a new client_id and re-create/re-attach to the workspace","On reconnect errors, re-attach promptly within the detach grace window","Persist and reuse client_id only while the server process is known to be the same one"],"exampleFix":"// before\nerr := backend.SendMessage(ctx, retiredID, msg)\n// after\nif errors.Is(err, backend.ErrClientRetired) {\n    clientID = uuid.NewString()\n    err = backend.SendMessage(ctx, clientID, msg)\n}","handlingStrategy":"fallback","validationCode":null,"typeGuard":"func isErrClientRetired(err error) bool {\n    return errors.Is(err, backend.ErrClientRetired)\n}","tryCatchPattern":"if err := backend.SendMessage(ctx, clientID, msg); err != nil {\n    if errors.Is(err, backend.ErrClientRetired) {\n        clientID = uuid.NewString() // fallback to fresh identity\n        return backend.SendMessage(ctx, clientID, msg)\n    }\n    return err\n}","preventionTips":["Reconnect within the detach grace (CRUSH_SERVER_DETACH_GRACE) after stream drops","Do not reuse a client_id across server restarts","Detect suspension/resume (network change) and proactively re-attach"],"tags":["lifecycle","client-id","reconnect"],"backgroundTag":"client-retired","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}