gastownhall/beads · error

recompute-blocked: %w

Error message

recompute-blocked: %w

What it means

ops.recompute failed with an error that is not a 'dirty graph' race. Dirty-graph errors (another process's uncommitted edit between pull and repair) are retried as transients; all other recompute failures abort the sync loop as hard errors.

Source

Thrown at cmd/bd/sync.go:328

			// through to the hard error below.
			if isPullBehindFastForwardableErr(pullErr) {
				out.Transients = append(out.Transients, syncTransient{
					Attempt: attempt, Kind: syncTransientPullBehind, Error: pullErr.Error(),
				})
				ops.report("pull behind a fast-forwardable remote tip (peer pushed after our fetch) — re-pulling and retrying")
				continue
			}
			return out, fmt.Errorf("pull: %w", pullErr)
		}
		if conflictErr != nil {
			return out, fmt.Errorf("conflict check: %w", conflictErr)
		}

		ops.report("recompute-blocked")
		corrected, err := ops.recompute(ctx)
		if err != nil {
			if !isRecomputeDirtyGraphErr(err) {
				return out, fmt.Errorf("recompute-blocked: %w", err)
			}
			// Not our failure and not a durable one: someone else's
			// uncommitted edit to issues/dependencies landed between our pull
			// and our repair. Treat it exactly like a push race — re-enter the
			// attempt loop, and if the budget runs out report the transient
			// exit so the next tick tries again. Classifying it as a hard
			// error instead left local commits unpublished until a tick
			// happened to catch a clean working set, which on a shared
			// sql-server topology is luck (wy-mlnz2).
			//
			// Two things about the retry are worth knowing before touching it.
			// It is paced by the pull's round trip, not by a sleep — the loop
			// has none. And the retry does not merely WAIT for the other
			// writer: the pull's own pre-merge auto-commit (GH#2474) stages and
			// commits whatever is dirty, so it is often what clears the guard,
			// committing that writer's already-SQL-committed rows under this
			// sync's author. That is pre-existing behavior on attempt 1 of
			// every tick and is data-safe, but a retry repeats the exposure —

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped error to find which graph invariant failed
  2. Fix or remove the offending dependency/issue rows (bd dep remove / bd doctor)
  3. Restore the DB from .beads/issues.jsonl export or git history if the graph is corrupted
  4. Retry sync after ensuring no other bd process holds the DB
Defensive patterns

Strategy: try-catch

Validate before calling

// validate the dependency graph before syncing
for _, edge := range allDeps {
    if !issueExists(edge.IssueID) || !issueExists(edge.DependsOnID) {
        return fmt.Errorf("dangling dependency %s -> %s; repair before sync", edge.IssueID, edge.DependsOnID)
    }
}

Try / catch

out, err := runSyncCommand(ctx, opts)
if err != nil {
    var recErr *RecomputeError
    if errors.As(err, &recErr) && !isDirtyGraphErr(recErr) {
        // hard failure: inspect graph corruption, run bd doctor
    }
    return err
}

Prevention

When it happens

Trigger: The graph-repair/recompute step hits a real failure: corrupted dependency graph rows, storage write error, constraint violation during repair, or context cancellation.

Common situations: Manually edited issues.jsonl or DB rows leaving the dependency graph inconsistent; disk-full during recompute; bug in a migration that produced edges pointing at nonexistent issues.

Related errors


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