gastownhall/beads · critical
unexpected conflict row with no issue id (safety check bypas
Error message
unexpected conflict row with no issue id (safety check bypassed)
What it means
resolveIssuesFieldMerge refuses to act on a merge plan entry whose our-side issue id is nil. The planning pass (issuesConflictsAreFieldMergeable) is supposed to decline rows without a key, so reaching this point means a safety invariant was violated — writing an UPDATE with a NULL key would resolve the wrong or no row. This is an internal-inconsistency guard, not an expected user-facing condition.
Source
Thrown at internal/storage/versioncontrolops/automerge.go:583
//
// DOLT_CONFLICTS_RESOLVE is table-level (--ours/--theirs), which cannot express
// a per-cell merge, so this uses dolt's manual-resolution path: write the
// merged values over our working-set row, then DELETE the conflict row — the
// delete is what tells dolt the row is settled, so it must come last. A row
// whose merge equals our side needs no write at all.
func resolveIssuesFieldMerge(ctx context.Context, db DBConn, plan []issuesRowMerge) error {
for _, m := range plan {
if len(m.lww) > 0 {
// Both sides edited these cells since the merge base, so one
// side's value was superseded by timestamp. That supersession is
// otherwise undiagnosable once the conflict row is gone — the same
// reason the config path names its resolved keys.
fmt.Fprintf(os.Stderr,
"Notice: auto-merged issue %v; %s settled last-write-wins (the older side's edit was superseded)\n",
m.ourKey, strings.Join(m.lww, ", "))
}
if m.ourKey == nil {
return fmt.Errorf("unexpected conflict row with no issue id (safety check bypassed)")
}
if len(m.columns) > 0 {
sets := make([]string, len(m.columns))
args := make([]any, 0, len(m.columns)+1)
for i, col := range m.columns {
// MySQL cannot bind an identifier and a peer's schema merge can
// extend the conflict table's columns, so gate every name the
// same way the table name is gated.
if err := ValidateConflictTable(col); err != nil {
return fmt.Errorf("refusing to write unexpected column %q of issues: %w", col, err)
}
sets[i] = fmt.Sprintf("`%s` = ?", col)
args = append(args, m.values[i])
}
args = append(args, m.ourKey)
stmt := fmt.Sprintf("UPDATE `issues` SET %s WHERE `%s` = ?", strings.Join(sets, ", "), issuesKeyColumn) //nolint:gosec // identifiers validated above
res, err := db.ExecContext(ctx, stmt, args...)
if err != nil {View on GitHub (pinned to 71377f2769)
Solutions
- Do not force-resolve; this error indicates the safety pre-checks were bypassed
- Resolve the merge manually with dolt table-level resolution (--ours/--theirs) after inspecting dolt_conflicts_issues
- File a bug with the conflict-row contents; the planner should have declined this row
- Check for beads/dolt version mismatch and align versions
Defensive patterns
Strategy: fallback
Validate before calling
// Inspect the live conflict rows before trusting auto-resolve
rows, err := db.QueryContext(ctx,
"SELECT our_id FROM dolt_conflicts_issues")
if err != nil { return err }
for rows.Next() {
var id any
_ = rows.Scan(&id)
if id == nil {
// delete/modify conflict shape: use manual resolution
return manualResolve(ctx, db)
}
}
rows.Close() Type guard
func planHasNilKey(plan []issuesRowMerge) bool {
for _, m := range plan {
if m.ourKey == nil { return true }
}
return false
} Try / catch
err := versioncontrolops.TryAutoResolveMergeConflicts(ctx, db)
if err != nil && strings.Contains(err.Error(), "no issue id (safety check bypassed)") {
// invariant broken: resolve manually with --ours/--theirs and report a bug
return manualResolve(ctx, db)
} Prevention
- Never force auto-resolve over delete/modify conflicts; resolve those manually
- Keep beads and dolt versions aligned so planner/resolver invariants hold
- Inspect dolt_conflicts_issues for NULL our-side rows before automating
- Report occurrences to maintainers — this path should be unreachable
When it happens
Trigger: Auto-resolving issues-table merge conflicts when a conflict row passed planning with a NULL 'our_id' — i.e. a delete/modify conflict row whose our-side is entirely NULL slipped past declineDuplicateConflictRows/planning, or an internal bug in plan construction.
Common situations: Merging a branch where an issue was deleted on one side and modified on the other (delete/modify conflicts) combined with an unexpected conflict-row shape; potential version mismatch between the planning and resolution code paths.
Related errors
- failed to get conflicts: %w
- failed to scan conflict: %w
- query conflicts for table %s: %w
- conflict columns for table %s: %w
- scan conflict row for table %s: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/10ada7abedf7cdde.
Report an issue: GitHub.