gastownhall/beads · error

rename lease row: %w

Error message

rename lease row: %w

What it means

After the issue row itself was renamed, the library moves any live lease row to the new ID (`UPDATE leases SET issue_id = ?`). This error wraps a failure of that update. The whole rename transaction rolls back, so the issue keeps its old ID and its lease.

Source

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

		UPDATE issues
		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 issue ID: %w", err)
	}
	if rows, _ := result.RowsAffected(); rows == 0 {
		return fmt.Errorf("issue not found: %s", oldID)
	}

	if err := UpdateIssueIDInDependenciesInTx(ctx, tx, oldID, newID); err != nil {
		return err
	}

	// A live lease follows its issue across the rename.
	if _, err := tx.ExecContext(ctx,
		`UPDATE leases SET issue_id = ? WHERE issue_id = ?`, newID, oldID); err != nil {
		return fmt.Errorf("rename lease row: %w", err)
	}

	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)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Retry the rename after the conflicting worker releases the lease (check wrapped error for lock timeout).
  2. Verify the leases table schema matches the current migration set.
  3. Check storage backend health/latency; increase lock timeout if the driver supports it.
  4. If a stale lease blocks the rename, release the lease through the supported API, then retry.

Example fix

// before
store.UpdateIssueID(ctx, oldID, newID, issue, actor) // fails: lease row locked by worker
// after
store.ReleaseLease(ctx, oldID) // or wait for lease expiry
err := store.UpdateIssueID(ctx, oldID, newID, issue, actor)
Defensive patterns

Strategy: retry

Validate before calling

if lease, _ := store.GetLease(ctx, oldID); lease != nil {
    // consider releasing or waiting for lease expiry before renaming
}

Try / catch

for attempt := 0; attempt < 3; attempt++ {
    err := store.UpdateIssueID(ctx, oldID, newID, issue, actor)
    if err == nil { break }
    if strings.Contains(err.Error(), "rename lease row:") && isLockTimeout(err) {
        time.Sleep(backoff(attempt)); continue
    }
    return err
}

Prevention

When it happens

Trigger: Calling UpdateIssueIDInTx while the issue holds an active lease and the leases UPDATE fails — driver error, lock contention on the leases row, or schema mismatch on the leases table.

Common situations: Another worker holds/contends the lease row concurrently; connection drop mid-transaction; leases table schema changed between versions; storage backend under heavy load causing lock timeouts.

Related errors


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