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

  1. Assign a non-empty ID (e.g. prefix-<generated>) to the issue before the transactional create
  2. Use the non-transactional create path that generates IDs automatically
  3. 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

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


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