- Moved collision-resolution-failure-analysis.md to docs/ (better organization with other architecture docs) - Created bd-191: Add --parent flag to bd list command Useful for listing children of an epic, will be even better with hierarchical IDs (bd-165) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
9.1 KiB
Collision Resolution Failure Analysis
Date: 2025-10-29
Incident: Import with --resolve-collisions created duplicate issues after routine git pull
The Incident
- User performed routine
git pullin~/src/fred/beads - JSONL file updated from remote (canonical database:
~/src/beads) - Import failed with collision detection for: bd-106, bd-108, bd-172, bd-175
- Used
--resolve-collisionswhich remapped them to bd-187, bd-188, bd-189, bd-190 - Result: Database now has BOTH versions - 6 duplicate issues created
The Evidence
Canonical Database (~/src/beads)
- Total issues: 509
- JSONL lines: 165
- bd-106: status=closed
- bd-108: status=closed
- bd-172: status=open
- bd-175: status=open
Polluted Database (~/src/fred/beads)
- Total issues: 515 (6 extra)
- JSONL lines: 171 (6 extra - includes the remapped duplicates)
- Original issues:
- bd-106: status=open (should be closed)
- bd-108: status=closed ✓
- bd-172: status=open ✓
- bd-175: status=open ✓
- Duplicate issues created:
- bd-187: "Import validation falsely reports data loss" (originally bd-106)
- bd-188: "Fix multi-round convergence" (duplicate of bd-108)
- bd-189: "Delete collision resolution code" (duplicate of bd-172)
- bd-190: "Test: N-clone scenario" (duplicate of bd-175)
The JSONL State
The JSONL file now contains BOTH versions:
- bd-106 (closed) - from remote
- bd-187 (open) - remapped local version
- bd-108, bd-188 - duplicates
- bd-172, bd-189 - duplicates
- bd-175, bd-190 - duplicates
Root Cause Analysis
What Should Have Happened
- Import detects bd-106 in JSONL has same ID as database
- Compares content: JSONL version is newer/different
- Updates the existing bd-106 in database (status: open → closed)
- No duplicates created
What Actually Happened
- Import detected bd-106 as a "collision"
- Collision resolution assumed: "Two different issues both want ID bd-106"
- Remapped local bd-106 → bd-187
- Imported JSONL bd-106 as new issue
- Result: Both versions now exist
The Fundamental Design Flaw
Collision resolution conflates two completely different scenarios:
Scenario A: Normal Update (NOT a collision)
- JSONL has bd-106 (status=closed)
- Database has bd-106 (status=open)
- This is a normal update - JSONL is source of truth after git pull
- Should: Update database bd-106 to match JSONL
- Should NOT: Treat as collision
Scenario B: Actual Collision (IS a collision)
- Branch A creates bd-106: "Fix authentication bug"
- Branch B creates bd-106: "Add new feature"
- Both branches merge into main
- This is a true collision - two different issues want same ID
- Should: Remap one to new ID, keep both
The Broken Logic
The current collision detection logic appears to be:
if (JSONL.id exists in database && JSONL.content != database.content) {
collision = true
}
This is catastrophically wrong because it treats every update as a collision.
What It Should Be
Idempotent import logic:
if (JSONL.id exists in database) {
if (JSONL.content_hash == database.content_hash) {
// Exact match - skip
} else {
// UPDATE the existing issue
database.update(JSONL.id, JSONL.content)
}
}
Collision detection (only for branch merges):
Collisions only occur when:
1. Git merge conflict in JSONL file (<<<<<<< markers present)
2. Two JSONL entries have same ID but different content
3. User explicitly wants to keep both versions
Why This Keeps Happening
-
Conceptual confusion: "Collision" is being used for two different things:
- Content difference (normal update)
- ID conflict (actual collision)
-
Wrong default: Import should default to "update on ID match"
- Current: Default to "collision on content difference"
-
Tool misuse:
--resolve-collisionsis being used for normal imports- Should only be needed for branch merge scenarios
-
No distinction: Code doesn't distinguish between:
- Import after
git pull(JSONL is authoritative) - Import after branch merge (need conflict resolution)
- Import after
The Correct Mental Model
Import Modes
-
Normal Import (default):
- Purpose: Sync database with JSONL (source of truth)
- Behavior: Update issues on ID match, create new ones
- Use case: After
git pull, switching branches, fresh clone - Should NEVER create duplicates
-
Collision Resolution Import:
- Purpose: Merge two independent databases that both created same IDs
- Behavior: Remap conflicting IDs to preserve both versions
- Use case: Branch merge where two devs independently created bd-42
- Creates duplicates BY DESIGN (but with different content)
The Missing Piece: Import Intent
The import command needs to know:
- "Trust the JSONL, update my database" (normal mode)
- "JSONL and database are both valid, resolve conflicts" (collision mode)
Current implementation assumes EVERY import is a collision scenario.
Immediate Impact
User now has:
- 6 duplicate issues polluting the database
- Incorrect JSONL synced to remote (if pushed)
- Canonical database potentially corrupted
- Zero trust in the import system
The Solution Architecture
Phase 1: Fix Import Default Behavior
// Default import: JSONL is source of truth
func Import(jsonlPath string) error {
for each issue in JSONL {
if issue.ID exists in DB {
if issue.content_hash == DB.content_hash {
skip // identical
} else {
UPDATE DB issue // JSONL wins
}
} else {
CREATE new issue
}
}
}
Phase 2: Collision Resolution (Separate Mode)
# Only when you KNOW you have a collision (branch merge)
bd import --resolve-collisions
# Should ONLY be used when:
# 1. Git shows merge conflict in JSONL
# 2. You want to preserve both versions
# 3. You understand duplicates will be created
Phase 3: Collision Detection (During Merge)
# Helper for detecting actual collisions in JSONL
bd detect-collisions
# Shows:
# - Issues with same ID, different content in JSONL conflict markers
# - Suggests resolution strategies
# - DOES NOT modify database
Testing Strategy
Test 1: Normal Update (Currently Broken)
# Setup: Database has bd-42 (status=open)
# JSONL has bd-42 (status=closed)
bd import -i issues.jsonl
# Expected: bd-42 updated to status=closed
# Actual: Collision detected, bd-42 remapped to bd-XXX
# FAILURE ❌
Test 2: Actual Collision
# Setup: Branch merge creates duplicate bd-42 in JSONL
# bd-42 (title="Fix bug") in HEAD
# bd-42 (title="Add feature") in BASE
bd import -i issues.jsonl --resolve-collisions
# Expected: Remap one to bd-XXX, keep both
# Actual: TBD
Test 3: Idempotent Import
# Import same JSONL twice
bd import -i issues.jsonl
bd import -i issues.jsonl
# Expected: Second import is no-op
# Actual: TBD
Recommended Actions
Immediate (User Recovery)
- Identify all duplicate pairs (bd-106/bd-187, etc.)
- Manually merge duplicates using
bd merge - Export clean database to JSONL
- Force push to reset remote
Short-term (Fix Import)
- Create bd-XXX: Rewrite import logic to default to UPDATE, not collision
- Create bd-XXX: Add
--merge-modeflag for actual collisions - Create bd-XXX: Write comprehensive import tests
- Create bd-XXX: Document when to use collision resolution
Long-term (Prevent Recurrence)
- Create bd-XXX: Add import validation (detect duplicates before committing)
- Create bd-XXX: Add
bd validatecommand to check database health - Create bd-XXX: Remove collision resolution entirely (use merge tools instead)
- Create bd-XXX: Implement content-addressable IDs to prevent collisions
Historical Context
This is NOT the first time this has happened:
- Multiple prior incidents of duplicate issues
- Repeated attempts to fix collision resolution
- bd-94: Epic to fix N-way collision convergence
- bd-109: Transaction + retry logic for collisions
- Pattern: We keep treating symptoms, not root cause
The Deeper Problem
beads is trying to solve distributed consensus without a consensus algorithm.
We're essentially trying to do what Git does (merge distributed changes) but:
- Git uses content-addressable storage (SHA hashes)
- Git has explicit merge semantics
- Git forces users to resolve conflicts manually
- beads assumes automatic resolution is possible
The N-way collision problem, the import pollution, the duplicate issues - they're all symptoms of trying to sync mutable IDs across independent actors.
Conclusion
The collision resolution feature is fundamentally broken because:
- It treats normal updates as collisions
- It has no concept of "source of truth"
- It creates duplicates when it should update
- It's being used for the wrong use case
The fix is not to improve collision resolution. The fix is to make normal import work correctly first. Then, collision resolution becomes a rare edge case for branch merges.
Until then, every git pull is a potential database corruption event.