gastownhall/beads · error

journal: record derived is_blocked update for %s: %w

Error message

journal: record derived is_blocked update for %s: %w

What it means

recordBlockedJournalChanges records an EventUpdate journal row for each issue whose derived is_blocked changed. If RecordEventInTx fails for any changed id, the error is wrapped with the issue id and the whole transaction aborts so the journal never misses a blocked-state transition.

Source

Thrown at internal/storage/issueops/journal.go:292

	var changed []blockedJournalKey
	for key, afterBlocked := range after {
		if beforeBlocked, existed := before[key]; existed && beforeBlocked != afterBlocked {
			changed = append(changed, key)
		}
	}
	sort.Slice(changed, func(i, j int) bool {
		if changed[i].table != changed[j].table {
			return changed[i].table < changed[j].table
		}
		return changed[i].id < changed[j].id
	})
	for _, key := range changed {
		// Derived maintenance runs from dozens of mutation paths (recompute /
		// mark passes) that carry no actor of their own; attributing the
		// triggering mutation here would mean threading an actor through the
		// whole blocked-state layer, so these rows record no actor.
		if err := RecordEventInTx(ctx, tx, EventUpdate, key.id, ""); err != nil {
			return fmt.Errorf("journal: record derived is_blocked update for %s: %w", key.id, err)
		}
	}
	return nil
}

// RecordEventInTx records op for issueID, snapshotting the issue's
// post-mutation state as of tx (read-your-writes within the same transaction).
// Use it for every op except delete (which has no surviving row — use
// RecordDeleteInTx) and dependency ops (use RecordDepEventInTx). A no-op when
// journaling is disabled.
//
// actor is the acting identity that performed the mutation, as resolved for
// the audit-events table; "" when the mutation path genuinely has none
// (derived maintenance, actorless delete plumbing). It is an explicit
// parameter, not ambient context, so a new call site cannot compile without
// deciding attribution.
func RecordEventInTx(ctx context.Context, tx DBTX, op EventOp, issueID, actor string) error {
	if !journalEnabled(ctx, tx) {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Identify the failing issue id from the wrapped message and the underlying cause
  2. Check whether the issue row was deleted concurrently (delete-before-recompute ordering)
  3. Ensure the journal table exists and is healthy
  4. Retry the operation; if deletes race recomputes, reorder so deletes complete first

Example fix

null
Defensive patterns

Strategy: try-catch

Validate before calling

// ensure all changed ids still exist before recompute+journal
existing, _ := issueops.ExistingIssueIDsInTableInTx(ctx, tx, "issues", changedIDs)
if len(existing) != len(changedIDs) { /* drop deleted ids first */ }

Type guard

null

Try / catch

if err := recordBlockedJournalChanges(ctx, tx, snapshot); err != nil {
  // wrapped msg includes the failing issue id
  log.Printf("blocked journal failed for %v", err)
  return err // transaction aborts to keep journal consistent
}

Prevention

When it happens

Trigger: One of the changed issue ids no longer exists (its row was deleted mid-transaction) so getJournalIssueInTx inside RecordEventInTx fails, or the journal insert itself hits a SQL error.

Common situations: Cascade deletes racing with blocked-state recompute; journal table missing/corrupt; connection failure during the journal write.

Related errors


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