gastownhall/beads · error
failed to increment issue counter for prefix %q: %w
Error message
failed to increment issue counter for prefix %q: %w
What it means
NextCounterIDTx increments the issue_counter row for a prefix via `UPDATE issue_counter SET last_id = last_id + 1 WHERE prefix = ?`. This error wraps any error returned by that SQL statement, meaning the counter UPDATE itself failed at the database driver level (not a missing row — that is handled later via rowsAffected). The wrapped driver error is the real cause.
Source
Thrown at internal/storage/issueops/helpers.go:233
return "", fmt.Errorf("failed to generate unique ID after trying lengths %d-%d with 10 nonces each", baseLength, maxLength)
}
// IsCounterModeTx checks whether issue_id_mode=counter is configured.
func IsCounterModeTx(ctx context.Context, tx DBTX) (bool, error) {
var idMode string
err := tx.QueryRowContext(ctx, "SELECT value FROM config WHERE `key` = ?", "issue_id_mode").Scan(&idMode)
if err != nil && err != sql.ErrNoRows {
return false, fmt.Errorf("failed to read issue_id_mode config: %w", err)
}
return idMode == "counter", nil
}
// NextCounterIDTx atomically increments and returns the next sequential issue ID.
func NextCounterIDTx(ctx context.Context, tx DBTX, prefix string) (string, error) {
res, err := tx.ExecContext(ctx, "UPDATE issue_counter SET last_id = last_id + 1 WHERE prefix = ?", prefix)
if err != nil {
return "", fmt.Errorf("failed to increment issue counter for prefix %q: %w", prefix, err)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return "", fmt.Errorf("failed to check rows affected for issue counter prefix %q: %w", prefix, err)
}
if rowsAffected == 0 {
if seedErr := SeedCounterFromExistingIssuesTx(ctx, tx, prefix); seedErr != nil {
return "", fmt.Errorf("failed to seed issue counter for prefix %q: %w", prefix, seedErr)
}
res, err = tx.ExecContext(ctx, "UPDATE issue_counter SET last_id = last_id + 1 WHERE prefix = ?", prefix)
if err != nil {
return "", fmt.Errorf("failed to increment issue counter after seeding for prefix %q: %w", prefix, err)
}
rowsAffected, err = res.RowsAffected()
if err != nil {
return "", fmt.Errorf("failed to check rows affected after seeding for prefix %q: %w", prefix, err)View on GitHub (pinned to 71377f2769)
Solutions
- Inspect the wrapped cause (%w) in the error chain; fix the underlying driver error first (connection, lock timeout, or schema).
- Verify the issue_counter table exists with expected schema: run a migration/schema check (e.g. bd doctor or the repo's migrate path) against the database.
- Retry issue creation — transient lock timeouts and deadlocks on the counter row usually resolve on retry.
- Check DB connectivity and server health (Dolt server running, correct DB path) if errors are connection-flavored.
- If contention is chronic, serialize issue creation for the same prefix (single writer per workspace) to avoid counter row hot-locking.
Example fix
// before: id, err := GenerateIssueIDInTable(ctx, tx, prefix, issue) // error unhandled. // after: id, err := GenerateIssueIDInTable(ctx, tx, prefix, issue); if err != nil { if isTransientLockErr(err) { return retryAfterBackoff(ctx) }; return fmt.Errorf("generate id: %w", err) } Defensive patterns
Strategy: try-catch
Validate before calling
var n int; if err := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'issue_counter'").Scan(&n); err != nil || n == 0 { return fmt.Errorf("issue_counter table missing; run migrations first") } Type guard
func isCounterUpdateErr(err error) bool { return err != nil && strings.Contains(err.Error(), "failed to increment issue counter for prefix") } Try / catch
id, err := GenerateIssueIDInTable(ctx, tx, prefix, issue); if err != nil { if isTransient(err) { return retryWithBackoff(ctx, func() error { _, err = GenerateIssueIDInTable(ctx, tx, prefix, issue); return err }) }; return fmt.Errorf("generate issue id: %w", err) } Prevention
- Keep schema migrations current so issue_counter exists before creating issues.
- Treat counter-row contention as transient and add retry with backoff.
- Monitor DB connection health before batch issue creation.
- Avoid multiple concurrent writers for the same prefix during initial load.
When it happens
Trigger: Calling NextCounterIDTx (via GenerateIssueIDInTable, i.e. issue creation in issue_id_mode=counter) when the issue_counter table is missing or corrupted, the connection to the Dolt/SQL server is broken or dropped mid-transaction, the transaction was already rolled back, a lock timeout or deadlock on the issue_counter row, or a schema mismatch (e.g. last_id column type changed).
Common situations: Database migration left the issue_counter table absent or altered; concurrent writers contending on the counter row causing lock wait timeouts; stale DB connection after network blip; embedded Dolt server restarted mid-create; opening a workspace whose .beads DB schema predates the counter table.
Related errors
- failed to increment issue counter after seeding for prefix %
- failed to begin transaction: %w
- failed to commit is_blocked repairs: %w
- failed to increment issue counter for prefix %q: %w
- db: ChildCounterSQLRepository.NextChildID: read counter for
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/921c078a57861d40.
Report an issue: GitHub.