gastownhall/beads · error

failed to record unclaim event: %w

Error message

failed to record unclaim event: %w

What it means

After a successful unclaim UPDATE, finishUnclaimInTx journals an 'unclaimed' event via RecordFullEventInTable and this wrapped error propagates if that journaling fails. The row mutation already succeeded inside the transaction; this failure prevents the audit trail from recording the release, so the whole transaction errors rather than committing an unjournaled release.

Source

Thrown at internal/storage/issueops/unclaim.go:128

}

// finishUnclaimInTx applies the post-UPDATE half of a release shared by
// UnclaimIssueInTx and UnclaimIssueIfAssigneeInTx: it drops the lease row (a
// no-op when none exists, e.g. a wisp or an open-but-assigned issue that was
// never leased) and records the "unclaimed" event. The row mutation
// (assignee/status/started_at/row_lock) must already have been applied in tx.
func finishUnclaimInTx(ctx context.Context, tx DBTX, eventTable string, id string, actor string, oldIssue *types.Issue) error {
	if err := DeleteLeaseInTx(ctx, tx, id); err != nil {
		return err
	}

	oldData, _ := json.Marshal(oldIssue)
	newData, _ := json.Marshal(map[string]interface{}{
		"assignee": "",
		"status":   "open",
	})
	if err := RecordFullEventInTable(ctx, tx, eventTable, id, "unclaimed", actor, string(oldData), string(newData)); err != nil {
		return fmt.Errorf("failed to record unclaim event: %w", err)
	}
	// A release changes assignee and status, so it journals as an update. Both
	// unclaim entry points funnel through here after their CAS succeeded, so
	// this covers the conditional release too.
	return RecordEventInTx(ctx, tx, EventUpdate, id, actor)
}

// UnclaimIssueIfAssigneeInTx atomically releases a claim only while the issue is
// still assigned to expectedAssignee — the compare-and-swap inverse of
// ClaimIssueInTx: a Go-side actorMatches precheck (ga-5ksp5) plus a conditional
// UPDATE CASed on row_lock, with RowsAffected as the verdict, so a stale
// releaser can never clobber a claim that has since moved to (or been
// re-taken by) someone else. "Still assigned to expectedAssignee" is judged
// under actorMatches, not verbatim equality, so a caller naming the current
// holder under a different layer's spelling of the same identity is a match,
// not a mismatch — see canonicalActor. On success it applies the same
// transition as UnclaimIssueInTx (assignee cleared, status reopened,
// started_at cleared, lease dropped, row_lock rewritten, "unclaimed" event

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped cause (%w) — fix the underlying DB error (disk space, schema, constraint, context cancellation).
  2. Verify the event table schema matches the current version (run bd migrate / upgrade the DB).
  3. Retry the whole unclaim in a fresh transaction; the CAS-based release is safe to re-run.
  4. If the event table is corrupted, restore from backup or rebuild event history before mutating claims.

Example fix

// before — swallowing the wrapped cause
err := issueops.UnclaimIssueInTx(ctx, tx, id, actor, false)
if err != nil { log.Print(err) }
// after — unwrap and diagnose the event-journal failure
if err != nil {
    log.Printf("unclaim failed: %v", errors.Unwrap(err))
    // e.g. check disk space / run schema migration, then retry
}
Defensive patterns

Strategy: try-catch

Try / catch

err := issueops.UnclaimIssueInTx(ctx, tx, id, actor, force)
if err != nil && strings.Contains(err.Error(), "failed to record unclaim event") {
    cause := errors.Unwrap(err)
    log.Printf("event journal failure, underlying cause: %v", cause)
    // fix DB (disk/schema) and retry the unclaim in a fresh tx
}

Prevention

When it happens

Trigger: RecordFullEventInTable fails due to an event-table schema mismatch, a disk-full/IO error in the underlying DB, a constraint violation on the event row, or a canceled context while writing the event.

Common situations: Old database missing the richer event columns; event table corrupted or locked by a migration; storage full so the event insert fails; context deadline exceeded during a long transaction.

Related errors


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