gastownhall/beads · error
ensure issue ID available: ID is empty
Error message
ensure issue ID available: ID is empty
What it means
EnsureIssueIDAvailableInTx rejects an empty issue ID before serializing the create. An ID is mandatory: the guard uses it to write a coordination key and probe the issues/wisps tables, and an empty ID would collide across all unnamed creates. This is a defensive pre-condition error, not a data-store failure.
Source
Thrown at internal/storage/issueops/create_only_guard.go:18
package issueops
import (
"context"
"crypto/sha256"
"fmt"
"strconv"
"github.com/steveyegge/beads/internal/storage"
)
// EnsureIssueIDAvailableInTx serializes same-shard creates and rejects occupied IDs.
func EnsureIssueIDAvailableInTx(ctx context.Context, tx DBTX, id string) error {
if tx == nil {
return fmt.Errorf("ensure issue ID available: transaction is nil")
}
if id == "" {
return fmt.Errorf("ensure issue ID available: ID is empty")
}
key := issueCreateCoordinationKey(id)
if _, err := tx.ExecContext(ctx, "REPLACE INTO local_metadata (`key`, value) VALUES (?, ?)", key, strconv.FormatInt(FreshRowLock(), 10)); err != nil {
return fmt.Errorf("coordinate issue create: %w", err)
}
for _, table := range []string{"issues", "wisps"} {
var count int
if err := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM "+table+" WHERE id = ?", id).Scan(&count); err != nil {
return fmt.Errorf("check %s for issue %q: %w", table, id, err)
}
if count > 0 {
return fmt.Errorf("%w: %s", storage.ErrAlreadyExists, id)
}
}
return nil
}
func issueCreateCoordinationKey(id string) string {View on GitHub (pinned to 71377f2769)
Solutions
- Assign a non-empty ID (e.g. prefix-<generated>) to the issue before the transactional create
- Use the non-transactional create path that generates IDs automatically
- Validate id != "" at the call site before opening the transaction
Example fix
// before
err := CreateIssueInTxWithResult(ctx, tx, issue) // issue.ID == ""
// after
if issue.ID == "" { issue.ID = GenerateID(prefix) }
err := CreateIssueInTxWithResult(ctx, tx, issue) Defensive patterns
Strategy: validation
Validate before calling
if issue.ID == "" {
return fmt.Errorf("issue %s needs an ID before transactional create", issue.Title)
} Type guard
func hasID(i *types.Issue) bool { return i != nil && i.ID != "" } Try / catch
if err := CreateIssueInTxWithResult(ctx, tx, issue); err != nil && strings.Contains(err.Error(), "ID is empty") {
// assign an ID and retry in a fresh transaction
} Prevention
- Generate IDs before entering transactions
- Centralize ID assignment in one helper
- Never reuse the transactional create path for auto-ID flows
When it happens
Trigger: Calling CreateIssueInTxWithResult (which calls this guard) with issue.ID == "" — e.g. an import path that assumed auto-ID generation but the transactional create path requires an explicit ID.
Common situations: Import code that forgot to assign IDs before the transactional create; refactored create flows that stopped defaulting IDs upstream.
Related errors
- db: Exists: id must not be empty
- config storage-class.%s: %w
- failed to create issue: %w
- invalid graph plan: %w
- list dep metadata: sourceID must not be empty
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/799a8c6e0506e2a5.
Report an issue: GitHub.