gastownhall/beads · warning

no live conflict for %s %s

Error message

no live conflict for %s %s

What it means

loadConflictRow found no matching row in dolt_conflicts_<table> for the given key, so there is nothing to resolve at row level. This is thrown when the key no longer corresponds to a live conflict — usually because the conflict was already resolved (ours/theirs, or by the peer) between listing conflicts and resolving this one. It is a stale-list condition, not a data corruption.

Source

Thrown at internal/storage/versioncontrolops/conflicts.go:321

// either side's key column so a row only one side has is *found* and then
// refused with a precise message, rather than reported as "no conflict".
func loadConflictRow(ctx context.Context, db DBConn, table, keyCol, key string) (rawConflictRow, error) {
	q := fmt.Sprintf("SELECT * FROM `dolt_conflicts_%s` WHERE `our_%s` = ? OR `their_%s` = ?", table, keyCol, keyCol) //nolint:gosec // identifiers validated
	rows, err := db.QueryContext(ctx, q, key, key)
	if err != nil {
		return rawConflictRow{}, fmt.Errorf("query conflict for %s %s: %w", table, key, err)
	}
	defer func() { _ = rows.Close() }()

	cols, err := rows.Columns()
	if err != nil {
		return rawConflictRow{}, fmt.Errorf("conflict columns for table %s: %w", table, err)
	}
	if !rows.Next() {
		if err := rows.Err(); err != nil {
			return rawConflictRow{}, fmt.Errorf("query conflict for %s %s: %w", table, key, err)
		}
		return rawConflictRow{}, fmt.Errorf("no live conflict for %s %s", table, key)
	}
	vals := make([]any, len(cols))
	ptrs := make([]any, len(cols))
	for i := range vals {
		ptrs[i] = &vals[i]
	}
	if err := rows.Scan(ptrs...); err != nil {
		return rawConflictRow{}, fmt.Errorf("scan conflict for %s %s: %w", table, key, err)
	}
	if rows.Next() {
		return rawConflictRow{}, fmt.Errorf("multiple conflict rows for %s %s; resolve the whole table instead", table, key)
	}
	return rawConflictRow{cols: cols, vals: vals}, errors.Join(rows.Err(), rows.Close())
}

// conflictTargetStillPresent reports whether key still names a row of table.
//
// It is the matched-rows check the resolvers need after a write, because

View on GitHub (pinned to 71377f2769)

Solutions

  1. Re-list the current conflicts (refresh the table view) and resolve only keys that still appear
  2. Check whether another session or process resolved the conflict concurrently
  3. If the merge was already committed or aborted, the conflict is gone — no action needed
  4. Retry the whole resolution pass atomically instead of replaying a stale key list

Example fix

// before
for _, key := range staleKeys {
    resolveOne(ctx, db, table, keyCol, key, strategy) // fails: no live conflict
}
// after
keys, err := listConflictKeys(ctx, db, table) // re-read live conflict rows
for _, key := range keys {
    resolveOne(ctx, db, table, keyCol, key, strategy)
}
Defensive patterns

Strategy: validation

Validate before calling

// re-list live conflicts immediately before resolving each key
keys, err := listConflictKeys(ctx, db, table)
if err != nil { return err }
resolveSet := intersect(pendingKeys, keys)

Try / catch

err := resolveOne(ctx, db, table, keyCol, key, strategy)
if err != nil && strings.Contains(err.Error(), "no live conflict for") {
    // already resolved elsewhere; refresh list and continue
    return refreshAndContinue()
}
return err

Prevention

When it happens

Trigger: ResolveConflictRows iterates a previously fetched conflict list and calls loadConflictRow for a key whose conflict row has already been deleted by an earlier resolution, a concurrent session, or because the merge was aborted/committed in the meantime.

Common situations: Running two `bd conflicts --theirs` sessions concurrently; resolving conflicts in a UI/CLI that cached the list; re-running a partially completed resolution script that already settled some rows.

Related errors


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