{"record":{"id":"10ada7abedf7cdde","repo":"gastownhall/beads","slug":"unexpected-conflict-row-with-no-issue-id-safety-c","errorCode":null,"errorMessage":"unexpected conflict row with no issue id (safety check bypassed)","messagePattern":"unexpected conflict row with no issue id \\(safety check bypassed\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"internal/storage/versioncontrolops/automerge.go","lineNumber":583,"sourceCode":"//\n// DOLT_CONFLICTS_RESOLVE is table-level (--ours/--theirs), which cannot express\n// a per-cell merge, so this uses dolt's manual-resolution path: write the\n// merged values over our working-set row, then DELETE the conflict row — the\n// delete is what tells dolt the row is settled, so it must come last. A row\n// whose merge equals our side needs no write at all.\nfunc resolveIssuesFieldMerge(ctx context.Context, db DBConn, plan []issuesRowMerge) error {\n\tfor _, m := range plan {\n\t\tif len(m.lww) > 0 {\n\t\t\t// Both sides edited these cells since the merge base, so one\n\t\t\t// side's value was superseded by timestamp. That supersession is\n\t\t\t// otherwise undiagnosable once the conflict row is gone — the same\n\t\t\t// reason the config path names its resolved keys.\n\t\t\tfmt.Fprintf(os.Stderr,\n\t\t\t\t\"Notice: auto-merged issue %v; %s settled last-write-wins (the older side's edit was superseded)\\n\",\n\t\t\t\tm.ourKey, strings.Join(m.lww, \", \"))\n\t\t}\n\t\tif m.ourKey == nil {\n\t\t\treturn fmt.Errorf(\"unexpected conflict row with no issue id (safety check bypassed)\")\n\t\t}\n\t\tif len(m.columns) > 0 {\n\t\t\tsets := make([]string, len(m.columns))\n\t\t\targs := make([]any, 0, len(m.columns)+1)\n\t\t\tfor i, col := range m.columns {\n\t\t\t\t// MySQL cannot bind an identifier and a peer's schema merge can\n\t\t\t\t// extend the conflict table's columns, so gate every name the\n\t\t\t\t// same way the table name is gated.\n\t\t\t\tif err := ValidateConflictTable(col); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"refusing to write unexpected column %q of issues: %w\", col, err)\n\t\t\t\t}\n\t\t\t\tsets[i] = fmt.Sprintf(\"`%s` = ?\", col)\n\t\t\t\targs = append(args, m.values[i])\n\t\t\t}\n\t\t\targs = append(args, m.ourKey)\n\t\t\tstmt := fmt.Sprintf(\"UPDATE `issues` SET %s WHERE `%s` = ?\", strings.Join(sets, \", \"), issuesKeyColumn) //nolint:gosec // identifiers validated above\n\t\t\tres, err := db.ExecContext(ctx, stmt, args...)\n\t\t\tif err != nil {","sourceCodeStart":565,"sourceCodeEnd":601,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/versioncontrolops/automerge.go#L565-L601","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"fallback","validationCode":"// Inspect the live conflict rows before trusting auto-resolve\nrows, err := db.QueryContext(ctx,\n    \"SELECT our_id FROM dolt_conflicts_issues\")\nif err != nil { return err }\nfor rows.Next() {\n    var id any\n    _ = rows.Scan(&id)\n    if id == nil {\n        // delete/modify conflict shape: use manual resolution\n        return manualResolve(ctx, db)\n    }\n}\nrows.Close()","typeGuard":"func planHasNilKey(plan []issuesRowMerge) bool {\n    for _, m := range plan {\n        if m.ourKey == nil { return true }\n    }\n    return false\n}","tryCatchPattern":"err := versioncontrolops.TryAutoResolveMergeConflicts(ctx, db)\nif err != nil && strings.Contains(err.Error(), \"no issue id (safety check bypassed)\") {\n    // invariant broken: resolve manually with --ours/--theirs and report a bug\n    return manualResolve(ctx, db)\n}","preventionTips":["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"],"tags":["dolt","merge-conflicts","invariant"],"backgroundTag":"merge-resolution-invariant-violated","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}