gastownhall/beads · error

merge conflicts require resolution (use --strategy ours|thei

Error message

merge conflicts require resolution (use --strategy ours|theirs)

What it means

Sync detected merge conflicts but was called without a resolution strategy, so it intentionally stops and leaves conflicts for manual resolution rather than guessing. The conflicts themselves are attached to result.Conflicts. This is a deliberate control-flow error telling the operator to re-run with --strategy ours|theirs or resolve by hand.

Source

Thrown at internal/storage/dolt/federation.go:387

	// Step 2: Get status before merge
	beforeCommit, _ := s.GetCurrentCommit(ctx) // Best effort: empty commit hash means diff won't be logged

	// Step 3: Merge peer's branch
	remoteBranch := fmt.Sprintf("%s/%s", peer, s.branch)
	conflicts, err := s.Merge(ctx, remoteBranch)
	if err != nil {
		result.Error = fmt.Errorf("merge failed: %w", err)
		return result, result.Error
	}

	// Step 4: Handle conflicts if any
	if len(conflicts) > 0 {
		result.Conflicts = conflicts

		if strategy == "" {
			// No strategy specified, leave conflicts for manual resolution
			result.Error = fmt.Errorf("merge conflicts require resolution (use --strategy ours|theirs)")
			return result, result.Error
		}

		// Auto-resolve using strategy
		for _, c := range conflicts {
			if err := s.ResolveConflicts(ctx, c.Field, strategy); err != nil {
				result.Error = fmt.Errorf("conflict resolution failed for %s: %w", c.Field, err)
				return result, result.Error
			}
		}
		result.ConflictsResolved = true

		// Commit the resolution INCLUDING config: the operator chose this
		// strategy, and plain Commit excludes config (GH#2455). A config-only
		// conflict — routine now that kv.memory.* memories sync through config —
		// would otherwise resolve but never commit, leaving the merge
		// unconcluded and re-wedging the next sync.
		if err := s.CommitMergeResolution(ctx, fmt.Sprintf("Resolve conflicts from %s using %s strategy", peer, strategy)); err != nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Re-run sync with an explicit strategy: ours keeps local rows, theirs takes peer rows
  2. Inspect result.Conflicts to see which fields conflict before choosing a strategy
  3. Resolve conflicts manually (ResolveConflicts per field) then commit and re-sync
  4. Prevent divergence by syncing more frequently so fewer concurrent edits accumulate
  5. Decide a house policy (e.g. last-writer-wins via 'theirs') for automated pipelines

Example fix

// before
result, err := store.Sync(ctx, "origin", "", nil)
// after
result, err := store.Sync(ctx, "origin", "theirs", nil)
if result != nil && len(result.Conflicts) > 0 {
    log.Printf("auto-resolved %d conflicts using theirs", len(result.Conflicts))
}
Defensive patterns

Strategy: fallback

Validate before calling

if strategy != "ours" && strategy != "theirs" && strategy != "" {
    return fmt.Errorf("invalid strategy %q", strategy)
}

Type guard

func isUnresolvedConflictsErr(err error) bool {
    return err != nil && strings.Contains(err.Error(), "require resolution")
}

Try / catch

result, err := store.Sync(ctx, peer, strategy, opts)
if isUnresolvedConflictsErr(err) && len(result.Conflicts) > 0 {
    // retry with a chosen strategy
    result, err = store.Sync(ctx, peer, "theirs", opts)
}

Prevention

When it happens

Trigger: store.Sync(ctx, peer, "" /* empty strategy */, opts) where Step 3's Merge returns a non-empty conflict list.

Common situations: Automated sync jobs that never pass a strategy hitting divergent edits to the same issue rows; two operators editing the same issue on different replicas; first-time federation setup where both sides created overlapping config/kv.memory entries.

Related errors


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