gastownhall/beads · error

failed to record event: %w

Error message

failed to record event: %w

What it means

After a successful issue update, updateIssueInTx records an audit event via RecordFullEventInTable and wraps any failure as "failed to record event: %w". The issue row may already be updated while the event is missing, so the caller's transaction semantics matter.

Source

Thrown at internal/storage/issueops/update.go:505

	//nolint:gosec // G201: issueTable comes from WispTableRouting (hardcoded constants)
	query := fmt.Sprintf("UPDATE %s SET %s WHERE id = ?", issueTable, strings.Join(setClauses, ", "))
	if _, err := tx.ExecContext(ctx, query, args...); err != nil {
		return nil, fmt.Errorf("failed to update issue: %w", err)
	}

	if clearLease {
		if err := DeleteLeaseInTx(ctx, tx, id); err != nil {
			return nil, err
		}
	}

	if recordEvent {
		oldData, _ := json.Marshal(oldIssue)
		newData, _ := json.Marshal(updates)
		eventType := DetermineEventType(oldIssue, updates)

		if err := RecordFullEventInTable(ctx, tx, eventTable, id, eventType, actor, string(oldData), string(newData)); err != nil {
			return nil, fmt.Errorf("failed to record event: %w", err)
		}
	}

	updateResult := &UpdateResult{OldIssue: oldIssue, IsWisp: isWisp, Changed: true, IssueRowsChanged: !isWisp, WispRowsChanged: isWisp}
	if rawStatus, hasStatus := updates["status"]; hasStatus {
		var newStatus string
		switch v := rawStatus.(type) {
		case string:
			newStatus = v
		case types.Status:
			newStatus = string(v)
		}
		oldActive := oldIssue.Status != types.StatusClosed && oldIssue.Status != types.StatusPinned
		newActive := newStatus != string(types.StatusClosed) && newStatus != string(types.StatusPinned)
		if oldActive != newActive {
			var affectedIssues, affectedWisps []string
			var aerr error
			if isWisp {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped error from RecordFullEventInTable for the root cause.
  2. Verify eventTable matches the issue/wisp routing for this id.
  3. Check the events table exists and has capacity for the JSON payloads (old/new data).
  4. Rely on transaction rollback so the issue update is also reverted; retry the whole update.

Example fix

// before
_, err := storage.UpdateIssueInTx(ctx, tx, issueTable, eventTable, id, updates, actor)
// after (verify routing before the call)
table, evTable := storage.WispTableRouting(isWisp)
_, err := storage.UpdateIssueInTx(ctx, tx, table, evTable, id, updates, actor)
Defensive patterns

Strategy: try-catch

Validate before calling

// ensure the event table exists/routing is correct before update
table, evTable := storage.WispTableRouting(isWisp)
if evTable == "" {
    return errors.New("no event table routed for this issue type")
}

Try / catch

if _, err := storage.UpdateIssueInTx(ctx, tx, table, evTable, id, updates, actor); err != nil {
    if strings.Contains(err.Error(), "failed to record event") {
        // whole tx should roll back; log and retry the full update
        return fmt.Errorf("event recording failed, update rolled back: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: RecordFullEventInTable fails inside the transaction: event table missing/corrupt, eventTable routing name wrong, oversized old/new JSON payloads exceeding column limits, or DB errors during the event INSERT.

Common situations: Schema missing the events table for wisps; extremely large issue payloads blowing past TEXT limits; Dolt transaction conflicts; passing a wrong eventTable constant for the routed table.

Related errors


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