gastownhall/beads · error

update wisp ID: %w

Error message

update wisp ID: %w

What it means

This error wraps a failure of the SQL UPDATE that renames a wisp row (`UPDATE wisps SET id = ? ...`). Wisps are ephemeral derived issues stored in a separate table; when the UPDATE fails at the driver level the rename transaction aborts and the wisp keeps its old ID.

Source

Thrown at internal/storage/issueops/bulk_ops.go:334

	return InsertDerivedEvent(ctx, tx, "events", AuxEvent{
		IssueID:   newID,
		EventType: "renamed",
		Actor:     actor,
		OldValue:  str(oldID),
		NewValue:  str(newID),
	})
}

func updateWispIDInTx(ctx context.Context, tx *sql.Tx, oldID, newID string, issue *types.Issue, actor string) error {
	now := time.Now().UTC()
	result, err := tx.ExecContext(ctx, `
		UPDATE wisps
		SET id = ?, title = ?, description = ?, design = ?, acceptance_criteria = ?, notes = ?, updated_at = ?
		WHERE id = ?
	`, newID, issue.Title, issue.Description, issue.Design, issue.AcceptanceCriteria, issue.Notes, now, oldID)
	if err != nil {
		return fmt.Errorf("update wisp ID: %w", err)
	}
	if rows, _ := result.RowsAffected(); rows == 0 {
		return fmt.Errorf("wisp not found: %s", oldID)
	}

	if err = InsertDerivedEvent(ctx, tx, "wisp_events", AuxEvent{
		IssueID:   newID,
		EventType: "renamed",
		Actor:     actor,
		OldValue:  str(oldID),
		NewValue:  str(newID),
	}); err != nil {
		return err
	}

	return UpdateWispIDInDependenciesInTx(ctx, tx, oldID, newID)
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Ensure newID does not collide with an existing wisp or issue before renaming.
  2. Validate the new ID format (non-empty, allowed characters).
  3. Inspect the wrapped driver error for the exact constraint and fix the input.
  4. Bring schema/migrations up to date and retry in a fresh transaction.

Example fix

// before
store.UpdateIssueID(ctx, oldID, dupID, issue, actor) // dupID exists in wisps -> constraint violation
// after
if err := ensureIDFree(ctx, store, dupID); err != nil {
    return fmt.Errorf("choose a different ID: %w", err)
}
store.UpdateIssueID(ctx, oldID, dupID, issue, actor)
Defensive patterns

Strategy: validation

Validate before calling

func validateWispRename(ctx context.Context, store Store, newID string) error {
    if newID == "" { return errors.New("new id must not be empty") }
    if existing, _ := store.GetIssue(ctx, newID); existing != nil { return fmt.Errorf("id %s already in use", newID) }
    return nil
}

Try / catch

if err := store.UpdateIssueID(ctx, oldID, newID, issue, actor); err != nil {
    if strings.Contains(err.Error(), "update wisp ID:") {
        return fmt.Errorf("wisp rename rejected (check ID collision/format): %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling UpdateIssueIDInTx on an issue that IsActiveWispInTx reports as a wisp, and `UPDATE wisps` fails — unique-key collision on newID in wisps, invalid newID, or a driver/connection error.

Common situations: Renaming to an ID that already exists as another wisp; wisp table schema drift after upgrade; connection loss mid-transaction; malformed target ID rejected by the storage engine.

Related errors


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