gastownhall/beads · warning · storage.ErrStatusMismatch

%w: %s has status %q, expected %q

Error message

%w: %s has status %q, expected %q

What it means

The status-mismatch branch of the CAS check: the caller declared an expected status but the row currently has a different one. storage.ErrStatusMismatch is returned with actual vs expected so state-machine transitions (e.g. close only if open) can fail safely instead of performing an invalid transition.

Source

Thrown at internal/storage/issueops/update_cas.go:56

	isWisp := IsActiveWispInTx(ctx, tx, id)
	issueTable, _, _, _ := WispTableRouting(isWisp)

	var assignee sql.NullString
	var status string
	err := tx.QueryRowContext(ctx,
		fmt.Sprintf("SELECT assignee, status FROM %s WHERE id = ?", issueTable), id,
	).Scan(&assignee, &status)
	if errors.Is(err, sql.ErrNoRows) {
		return fmt.Errorf("%w: issue %s", storage.ErrNotFound, id)
	}
	if err != nil {
		return fmt.Errorf("failed to read assignee/status for %s: %w", id, err)
	}
	if expectedAssignee != nil && !actorMatches(assignee.String, *expectedAssignee) {
		return fmt.Errorf("%w: %s is held by %q, expected %q", storage.ErrAssigneeMismatch, id, assignee.String, *expectedAssignee)
	}
	if expectedStatus != nil && status != *expectedStatus {
		return fmt.Errorf("%w: %s has status %q, expected %q", storage.ErrStatusMismatch, id, status, *expectedStatus)
	}
	return nil
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check errors.Is(err, storage.ErrStatusMismatch) and treat the current status (shown in the message) as authoritative
  2. Re-fetch the issue and skip the transition if it already reached the target state (idempotent handling)
  3. Retry only after reconciling with the actual status, never in a tight loop
  4. Use CheckVersionInTx/row-version CAS when the whole row, not just status, must be unchanged

Example fix

// before
closeIssue(id) // fails on second run
// after
err := closeIssue(id)
if errors.Is(err, storage.ErrStatusMismatch) {
    iss, _ := store.GetIssue(ctx, id)
    if iss.Status == "closed" { return nil } // already closed; idempotent
}
Defensive patterns

Strategy: try-catch

Validate before calling

iss, _ := store.GetIssue(ctx, id)
if iss.Status != "open" { return fmt.Errorf("issue %s is %s; refusing transition", id, iss.Status) }

Try / catch

err := closeIssueChecked(ctx, tx, id, ver)
if errors.Is(err, storage.ErrStatusMismatch) {
    return nil // already in/checked past target status; idempotent no-op
}

Prevention

When it happens

Trigger: ExecuteUpdate with ExpectedFields.Status (commonly 'open' before a close) while the issue is already closed/reopened/in another status — usually because another writer changed it first.

Common situations: Double-close attempts from two agents; retry after a partially-completed close; scripts assuming 'open' on issues already resolved; workflow automation racing human edits.

Related errors


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