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

  1. Message the holder first: 'bd mail <assignee>' to coordinate
  2. Use 'bd reclaim' to properly take over the claim
  3. Pass --force only if the holder's claim is abandoned (crashed agent, expired lease)
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/5498ead2601ae462. Report an issue: GitHub.