gastownhall/beads · error
cannot reassign %s: held by %q (in_progress); coordinate wit
Error message
cannot reassign %s: held by %q (in_progress); coordinate with the holder (bd mail %s) — pass --force only if their claim is abandoned (crashed agent, expired lease), or use bd reclaim
What it means
The reassignment validator fences issues currently in 'in_progress' status. The issue is actively held by an assignee (outside the shared pool-alias set), so reassignment is rejected to avoid stealing a live claim.
Source
Thrown at internal/validation/issue.go:220
// cross-actor takeovers without adding new identity guarantees, and without
// falsely treating the current holder as a stranger when actor or
// newAssignee names it under a different layer's spelling of the same
// identity.
func AssigneeNotStolen(actor, newAssignee string, poolAliases func() []string, force bool) IssueValidator {
return func(id string, issue *types.Issue) error {
if issue == nil || force {
return nil
}
if issue.Assignee == "" || ActorMatches(issue.Assignee, actor) || ActorMatches(issue.Assignee, newAssignee) {
return nil
}
if issue.Status != types.StatusInProgress {
return nil
}
if poolAliases != nil && slices.Contains(poolAliases(), issue.Assignee) {
return nil
}
return fmt.Errorf("cannot reassign %s: held by %q (in_progress); coordinate with the holder (bd mail %s) — pass --force only if their claim is abandoned (crashed agent, expired lease), or use bd reclaim", id, issue.Assignee, issue.Assignee)
}
}
// NotClosed validates that an issue is not already closed.
func NotClosed() IssueValidator {
return func(id string, issue *types.Issue) error {
if issue == nil {
return nil
}
if issue.Status == types.StatusClosed {
return fmt.Errorf("issue %s is already closed", id)
}
return nil
}
}
// NotHooked validates that an issue is not in hooked status.
func NotHooked(force bool) IssueValidator {View on GitHub (pinned to 71377f2769)
Solutions
- Message the holder first: 'bd mail <assignee>' to coordinate
- Use 'bd reclaim' to properly take over the claim
- Pass --force only if the holder's claim is abandoned (crashed agent, expired lease)
- If the issue is genuinely free, have the holder set it back from in_progress before reassigning
Example fix
// before bd update bd-42 assignee=agent-2 // held by agent-1 in_progress → blocked // after bd reclaim bd-42 # or: bd mail agent-1 to coordinate bd update bd-42 assignee=agent-2
Defensive patterns
Strategy: validation
Validate before calling
if issue.Status == types.StatusInProgress && !slices.Contains(poolAliases(), issue.Assignee) {
return fmt.Errorf("%s is in_progress and held by %s; reclaim or coordinate first", issue.ID, issue.Assignee)
} Type guard
func isLiveClaim(issue *types.Issue, poolAliases func() []string) bool {
return issue != nil && issue.Status == types.StatusInProgress &&
(poolAliases == nil || !slices.Contains(poolAliases(), issue.Assignee))
} Try / catch
if err := reassign(id, newAssignee); err != nil {
if strings.Contains(err.Error(), "held by") {
// verify holder is gone (crashed/expired) before forcing
if err := reclaim(id); err != nil { return err }
return reassign(id, newAssignee)
}
return err
} Prevention
- Check issue.Status before reassigning; only reassign open issues freely
- Message the holder with 'bd mail <assignee>' before taking over in_progress work
- Use --force only for provably abandoned claims (crashed agent, expired lease)
- Prefer 'bd reclaim' as the sanctioned takeover path
When it happens
Trigger: An assignee update on an issue with Status=in_progress, where the current assignee is not in the poolAliases allowlist (i.e. a real individual claim, not a shared pool identity).
Common situations: Orchestrators re-dispatching an issue already claimed by a running agent; a human reassigning work without noticing the holder is mid-task; crashed agents leaving stale in_progress claims (the intended --force case).
Related errors
- ErrNotClaimable
- cannot close %s: assignee is %q, actor is %q; reclaim or use
- ErrAlreadyClaimed
- Unclaim: id must not be empty
- UnclaimIfAssignee: id must not be empty
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/5498ead2601ae462.
Report an issue: GitHub.