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
- Check AdaptiveIDConfig MaxLength — raise it if it was shrunk below sane values (e.g. 6+).
- Ensure issue.CreatedAt is set to the real timestamp and not pinned/zero so nonces hash differently.
- Vary the issue Title/Description or mint with an explicit ID for bulk duplicates.
- 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
- Never pin CreatedAt across bulk inserts; let each record get its own timestamp.
- Keep AdaptiveIDConfig.MaxLength at the default (don't shrink below 6).
- Use explicit IDs for bulk imports with duplicate titles.
- Monitor for large counts per prefix; adaptive lengths should grow automatically.
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
- failed to generate unique ID after trying lengths %d-%d with
- failed to generate unique ID after trying lengths %d-%d with
- db: NextCounterID: prefix must not be empty
- generating Linear milestone epic ID: %w
- artifact collision for %s: %s already exists with different
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/5a1de92f659d20ff.
Report an issue: GitHub.