gastownhall/beads · warning
cannot unclaim closed issue %s
Error message
cannot unclaim closed issue %s
What it means
UnclaimIssueInTx refuses to release an issue whose status is closed, returning this plain error. The library throws it because unclaiming is defined as clearing the assignee and resetting status to open; doing that to a closed issue would mutate finished work. Claim release and issue closure are separate lifecycle steps.
Source
Thrown at internal/storage/issueops/unclaim.go:48
// - Issue has no assignee (nothing to unclaim)
// - Issue is claimed by a different actor and force is false (ErrNotOwner)
//
//nolint:gosec // G201: table names come from WispTableRouting (hardcoded constants)
func UnclaimIssueInTx(ctx context.Context, tx DBTX, id string, actor string, force bool) error {
// Route to the correct table (issues/wisps) automatically, matching
// ClaimIssueInTx — a wisp claim lives in the wisp tables, so its release
// must update them too rather than no-op against the permanent issues table.
isWisp := IsActiveWispInTx(ctx, tx, id)
issueTable, _, eventTable, _ := WispTableRouting(isWisp)
oldIssue, err := GetIssueInTx(ctx, tx, id)
if err != nil {
return fmt.Errorf("failed to get issue for unclaim: %w", err)
}
// Validate: cannot unclaim closed issues
if oldIssue.Status == types.StatusClosed {
return fmt.Errorf("cannot unclaim closed issue %s", id)
}
// Validate: must have an assignee to unclaim
if oldIssue.Assignee == "" {
return fmt.Errorf("issue %s is not assigned", id)
}
// Validate ownership unless the caller forced the release. Without force, a
// process may only release its own claim. Compared under actorMatches, not
// verbatim, so a caller naming its own identity under a different layer's
// spelling (ga-5ksp5) is not refused as a stranger.
if !force && !actorMatches(oldIssue.Assignee, actor) {
return fmt.Errorf("%w: %s is held by %s; coordinate with the holder — pass --force only if their claim is abandoned (crashed agent, expired lease)",
storage.ErrNotOwner, id, oldIssue.Assignee)
}
now := time.Now().UTC()
View on GitHub (pinned to 71377f2769)
Solutions
- Skip the release when the issue is already closed — nothing needs unclaiming.
- Check status first via GetIssue and only call Unclaim for open/in_progress issues.
- If the claim must be cleared on a closed issue, deliberately reopen it, then unclaim.
- Treat this error as benign/no-op in cleanup paths rather than failing shutdown.
Example fix
// before
if err := store.ReleaseIssue(ctx, id); err != nil { return err }
// after
iss, err := store.GetIssue(ctx, id)
if err != nil { return err }
if iss.Status != types.StatusClosed {
if err := store.ReleaseIssue(ctx, id); err != nil { return err }
} Defensive patterns
Strategy: validation
Validate before calling
iss, err := store.GetIssue(ctx, id)
if err != nil { return err }
if iss.Status == types.StatusClosed {
return nil // nothing to unclaim
} Try / catch
if err := store.ReleaseIssue(ctx, id); err != nil {
if strings.Contains(err.Error(), "cannot unclaim closed issue") {
return nil // already closed; expected during cleanup
}
return err
} Prevention
- In agent shutdown hooks, check issue status before releasing claims.
- Order cleanup: unclaim first, close last.
- Make release paths idempotent and tolerant of already-closed issues.
When it happens
Trigger: Calling ReleaseIssueInTx/UnclaimIssueInTx on an issue with Status == types.StatusClosed, e.g. an agent that closed the issue on completion and then attempts to release its claim during cleanup.
Common situations: Double cleanup: close-on-completion plus a shutdown hook that releases claims; stale batch scripts releasing claims for already-closed issues.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- issue %s is not assigned
- errIdleTimeout
- server: ExternalDoltServer.Start: server already started
- ErrStoreClosed
- errClosed
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/0ecb1a772adc90f4.
Report an issue: GitHub.