gastownhall/beads · error

db: IssueSQLRepository.UnclaimIssue: %w

Error message

db: IssueSQLRepository.UnclaimIssue: %w

What it means

Wraps a failure from issueops.UnclaimIssueInTx when releasing an issue/wisp claim unconditionally. The helper routes to issues or wisps tables automatically; failures can be lookup errors, SQL exec errors, or 'not claimed' style validation errors from finishUnclaimInTx. The wrapper adds repository context only.

Source

Thrown at internal/storage/domain/db/issue.go:1216

func (r *issueSQLRepositoryImpl) GetStaleIssues(ctx context.Context, filter types.StaleFilter) ([]*types.Issue, error) {
	out, err := issueops.GetStaleIssuesInTx(ctx, r.runner, filter)
	if err != nil {
		return nil, fmt.Errorf("db: IssueSQLRepository.GetStaleIssues: %w", err)
	}
	return out, nil
}

func (r *issueSQLRepositoryImpl) GetEpicsEligibleForClosure(ctx context.Context) ([]*types.EpicStatus, error) {
	out, err := issueops.GetEpicsEligibleForClosureInTx(ctx, r.runner)
	if err != nil {
		return nil, fmt.Errorf("db: IssueSQLRepository.GetEpicsEligibleForClosure: %w", err)
	}
	return out, nil
}

func (r *issueSQLRepositoryImpl) UnclaimIssue(ctx context.Context, id, actor string, force bool) error {
	if err := issueops.UnclaimIssueInTx(ctx, r.runner, id, actor, force); err != nil {
		return fmt.Errorf("db: IssueSQLRepository.UnclaimIssue: %w", err)
	}
	return nil
}

// UnclaimIssueIfAssignee runs the classic compare-and-swap release against this
// runner. Like UnclaimIssue it takes no IssueTableOpts: issueops routes the
// write to the issues or wisps tables from the row itself, so a wisp's claim is
// released against the wisp tables on both backends. The mismatch verdict
// (storage.ErrAssigneeMismatch, nothing written) is produced by the shared
// helper, not restated here.
func (r *issueSQLRepositoryImpl) UnclaimIssueIfAssignee(ctx context.Context, id, actor, expectedAssignee string) error {
	if err := issueops.UnclaimIssueIfAssigneeInTx(ctx, r.runner, id, actor, expectedAssignee); err != nil {
		return fmt.Errorf("db: IssueSQLRepository.UnclaimIssueIfAssignee: %w", err)
	}
	return nil
}

// HeartbeatIssue refreshes the lease on an issue actor holds in_progress,

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped cause to distinguish lookup/exec failures from validation failures.
  2. Check the issue still exists and has an active claim before releasing (or handle idempotently).
  3. Retry on transient driver errors; the transaction rolls back cleanly.
  4. Verify the lease/claim tables exist and the schema is current.

Example fix

// before
if err := repo.UnclaimIssue(ctx, id, actor, false); err != nil { return err }
// after
if err := repo.UnclaimIssue(ctx, id, actor, false); err != nil {
    if strings.Contains(err.Error(), "not claimed") { return nil } // already released
    return fmt.Errorf("unclaim %s: %w", id, err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

// confirm the claim exists before releasing
issue, err := repo.Get(ctx, id)
if err != nil || issue.Assignee == "" { return nil } // nothing to unclaim

Try / catch

if err := repo.UnclaimIssue(ctx, id, actor, false); err != nil {
    if isAlreadyReleased(err) { return nil } // idempotent no-op
    return fmt.Errorf("unclaim %s: %w", id, err)
}

Prevention

When it happens

Trigger: Calling UnclaimIssue(ctx, id, actor, force) when: the issue lookup fails (GetIssueInTx error), the UPDATE/DELETE of the lease row fails at the driver level, the ID is empty/invalid, or finishing the unclaim (event write, rows-affected check) errors.

Common situations: Concurrent modification (claim already released) surfacing as an unexpected rows-affected result, connection loss mid-transaction, or targeting an ID that was deleted between check and write.

Related errors


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