gastownhall/beads · error

failed to resolve claim pools: %w

Error message

failed to resolve claim pools: %w

What it means

ClaimIssueInTx resolves claim pool aliases (pool pseudo-assignees like 'fable-crew' that any actor may claim through) via ClaimPoolAliasesInTx; failure is wrapped as "failed to resolve claim pools: %w". Pools enable dispatcher pre-assignment; without resolving them the pool-aware CAS cannot be built. The error means reading the claim.pools config from the DB failed.

Source

Thrown at internal/storage/issueops/claim.go:77

	rowLockClause, rowLockArgs := RowLockClause()

	// An issue is claimable from "open" plus any configured custom status whose
	// category is "active" (e.g. a draft->ready->in_progress lifecycle where
	// "ready" should be claimable). WIP/done/frozen customs are excluded so the
	// anti-steal protection from GH-3570 is preserved.
	claimableStatuses, err := ClaimableSourceStatusesInTx(ctx, tx)
	if err != nil {
		return nil, fmt.Errorf("failed to resolve claimable statuses: %w", err)
	}
	statusPlaceholders, statusArgs := buildSQLInClause(claimableStatuses)

	// Pool-aware claim (bd-bguz6): a dispatcher may pre-assign issues to a
	// pool pseudo-assignee (e.g. "fable-crew"). Aliases listed in the
	// claim.pools config are claimable by any actor through the same CAS;
	// issues assigned to a real actor keep their anti-steal protection.
	pools, err := ClaimPoolAliasesInTx(ctx, tx)
	if err != nil {
		return nil, fmt.Errorf("failed to resolve claim pools: %w", err)
	}

	// Claimability of the assignee slot, judged in Go against oldIssue (the
	// pre-image read above, inside this same transaction) rather than as a
	// spelling-sensitive SQL predicate (ga-v2k49, same shape as ga-5ksp5's
	// unclaim.go fix): empty/unassigned, already this actor — including an
	// idempotent re-claim spelled under a different layer's separator
	// convention (ga-wzl83) — or assigned to a claim-pool alias. That last
	// membership test is deliberately exact-string, unlike actorMatches right
	// beside it: a pool alias (e.g. "fable-crew") is a literal claim.pools
	// config value, not a Gas Town identity that gets respelled per layer, so
	// there is no cross-spelling variant to reconcile — canonicalizing it
	// could only blur two administratively-distinct pool names into one.
	assigneeOK := oldIssue.Assignee == "" || actorMatches(oldIssue.Assignee, actor) || slices.Contains(pools, oldIssue.Assignee)

	// Conditional UPDATE: only attempted while the issue was still claimable
	// as of oldIssue, and even then CASed on row_lock rather than re-checking
	// assignee/status in SQL — row_lock is rewritten by every path that

View on GitHub (pinned to 71377f2769)

Solutions

  1. Migrate/upgrade the database so the pool-alias config storage exists.
  2. Retry the claim transaction if the wrapped error is transient (connection/lock).
  3. Validate the claim.pools config (remove malformed entries) and re-check with bd doctor.
  4. Check the wrapped error text to distinguish missing-table (migrate) vs timeout (contention).
Defensive patterns

Strategy: retry

Validate before calling

if err := validatePoolsConfig(cfg.Claim.Pools); err != nil {
    return fmt.Errorf("invalid claim.pools config: %w", err)
}

Try / catch

if err != nil {
    tx.Rollback()
    return retry(fmt.Errorf("claim pool resolution failed: %w", err))
}

Prevention

When it happens

Trigger: Claiming when: the pool configuration storage is missing or unreadable, the transaction is aborted by an earlier error, the context is cancelled, or the connection fails during the config SELECT.

Common situations: Older databases without the claim-pool config storage; malformed claim.pools configuration after manual editing; dispatcher loops hitting transient DB errors under load.

Related errors


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