gastownhall/beads · error

failed to generate unique ID for prefix %q after lengths %d.

Error message

failed to generate unique ID for prefix %q after lengths %d..%d with 10 nonces each

What it means

Hash-ID minting exhausted all retries: for every length from the adaptive base length up to cfg.MaxLength, 10 nonce variants were generated and every candidate already existed in the target table. This means the hash space for that prefix/title/actor/createdAt combination is effectively saturated — astronomically unlikely in normal use.

Source

Thrown at internal/storage/domain/issue.go:1642

	}
	baseLength := ComputeAdaptiveLength(count, cfg)
	if baseLength > cfg.MaxLength {
		baseLength = cfg.MaxLength
	}

	for length := baseLength; length <= cfg.MaxLength; length++ {
		for nonce := 0; nonce < 10; nonce++ {
			candidate := idgen.GenerateHashID(prefix, issue.Title, issue.Description, actor, issue.CreatedAt, length, nonce)
			exists, err := u.issueRepo.Exists(ctx, candidate, tableOpts)
			if err != nil {
				return "", err
			}
			if !exists {
				return candidate, nil
			}
		}
	}
	return "", fmt.Errorf("failed to generate unique ID for prefix %q after lengths %d..%d with 10 nonces each", prefix, baseLength, cfg.MaxLength)
}

func (u *issueUseCaseImpl) CloseIssue(ctx context.Context, id string, params CloseIssueParams, actor string) (CloseIssueResult, error) {
	return u.close(ctx, id, params, actor, false)
}

func (u *issueUseCaseImpl) CloseWisp(ctx context.Context, id string, params CloseIssueParams, actor string) (CloseIssueResult, error) {
	return u.close(ctx, id, params, actor, true)
}

// CloseIssueChecked closes an issue through the shared guarded close path.
func (u *issueUseCaseImpl) CloseIssueChecked(ctx context.Context, id string, params CloseIssueParams, actor string, force bool) (CloseIssueResult, error) {
	return u.closeChecked(ctx, id, params, actor, force, false)
}

// CloseWispChecked is the wisp twin of CloseIssueChecked.
func (u *issueUseCaseImpl) CloseWispChecked(ctx context.Context, id string, params CloseIssueParams, actor string, force bool) (CloseIssueResult, error) {
	return u.closeChecked(ctx, id, params, actor, force, true)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check AdaptiveIDConfig MaxLength — raise it if it was shrunk below sane values (e.g. 6+).
  2. Ensure issue.CreatedAt is set to the real timestamp and not pinned/zero so nonces hash differently.
  3. Vary the issue Title/Description or mint with an explicit ID for bulk duplicates.
  4. If truly colliding, inspect CountForPrefix/ComputeAdaptiveLength behavior for your issue volume.

Example fix

// before: deterministic retries collide
issue.CreatedAt = fixedTime // same for all inserts

// after
issue.CreatedAt = time.Now().UTC() // per-record timestamp varies the hash
Defensive patterns

Strategy: retry

Validate before calling

cfg, err := cfgRepo.GetAdaptiveIDConfig(ctx)
if err == nil && cfg.MaxLength < 6 {
    return errors.New("adaptive MaxLength too small; collision risk high — raise it")
}
if issue.CreatedAt.IsZero() {
    issue.CreatedAt = time.Now().UTC()
}

Try / catch

var errCollision = errors.New("failed to generate unique ID")
if err != nil && strings.Contains(err.Error(), "failed to generate unique ID") {
    issue.CreatedAt = time.Now().UTC() // re-randomize hash inputs
    return mintTopLevelID(ctx, issue, actor, useWisp) // one retry
}

Prevention

When it happens

Trigger: mintTopLevelID loop (issue.go:1630-1641) completing without returning: Exists() true for all candidates — e.g. adversarial duplication (identical title/description/actor/CreatedAt reused), pathological AdaptiveIDConfig with a tiny MaxLength, or a bug freezing CreatedAt across retries.

Common situations: Script mass-creating issues with identical titles and a pinned CreatedAt; misconfigured MaxLength set to 1-2 chars; automated import re-running with deterministic inputs.

Related errors


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