{"record":{"id":"b04f71e44986a8cc","repo":"charmbracelet/crush","slug":"workspace-closing","errorCode":null,"errorMessage":"workspace closing","messagePattern":"workspace closing","errorType":"exception","errorClass":"ErrWorkspaceClosing","httpStatus":null,"severity":"error","filePath":"internal/backend/backend.go","lineNumber":39,"sourceCode":"\t\"github.com/charmbracelet/crush/internal/db\"\n\t\"github.com/charmbracelet/crush/internal/proto\"\n\t\"github.com/charmbracelet/crush/internal/skills\"\n\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\".","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/backend/backend.go#L21-L57","documentation":"ErrWorkspaceClosing reports that a message was submitted to a workspace that is in the process of being torn down. The backend refuses new work once it has committed to closing, because the agent coordinator will not be available to process it. Returned by SendMessage and mapped through handleError.","triggerScenarios":"Calling SendMessage on a workspace whose teardown has begun — the client's claim was released, detach grace expired, or an explicit DeleteWorkspace raced an in-flight send.","commonSituations":"Racing a clean exit: a background goroutine sends a final message while the main path tears the workspace down; a proxy timeout drops the SSE stream and the workspace closes before reconnect.","solutions":["Retry the send against a newly created workspace after treating the old one as gone","Serialize teardown and sends so SendMessage completes before DeleteWorkspace is invoked","Keep the attachment (SSE stream) open for the duration of message traffic"],"exampleFix":"// before\nif err := backend.SendMessage(ctx, clientID, msg); err != nil { return err }\n// after\nif err := backend.SendMessage(ctx, clientID, msg); err != nil {\n    if errors.Is(err, backend.ErrWorkspaceClosing) {\n        ws, err := backend.CreateWorkspace(ctx, clientID, path)\n        if err != nil { return err }\n        return backend.SendMessage(ctx, clientID, msg)\n    }\n    return err\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"func isErrWorkspaceClosing(err error) bool {\n    return errors.Is(err, backend.ErrWorkspaceClosing)\n}","tryCatchPattern":"if err := backend.SendMessage(ctx, clientID, msg); err != nil {\n    if errors.Is(err, backend.ErrWorkspaceClosing) {\n        return recreateWorkspaceAndResend(ctx, clientID, path, msg)\n    }\n    return err\n}","preventionTips":["Drain in-flight SendMessage calls before invoking DeleteWorkspace","Keep the SSE attachment open until all message traffic completes","Avoid background goroutines sending messages after teardown begins"],"tags":["lifecycle","workspace","race-condition"],"backgroundTag":"workspace-shutting-down","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}