When multiple clones commit to beads-sync branch and histories diverge, git merge would fail. This replaces git's commit-level merge with a content-based merge that extracts JSONL from base/local/remote and merges at the semantic level. Key changes: - Add divergence detection using git rev-list --left-right - Extract JSONL content from specific commits for 3-way merge - Reset to remote's history then commit merged content on top - Pre-emptive fetch before commit to reduce divergence likelihood - Deletions.jsonl merged by union (keeps all deletions) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
191 lines
7.3 KiB
Markdown
191 lines
7.3 KiB
Markdown
# BD-3S8: Multi-Clone Sync Fix
|
|
|
|
## Problem
|
|
|
|
When multiple clones of a repository both commit to the `beads-sync` branch and one tries to pull, git's merge would fail due to diverged histories. This made multi-clone workflows unreliable.
|
|
|
|
## Solution
|
|
|
|
Replace git's commit-level merge with a content-based merge that handles divergence gracefully:
|
|
|
|
1. **Fetch** (not pull) from remote
|
|
2. **Detect divergence** using `git rev-list --left-right --count`
|
|
3. **Extract JSONL** from merge base, local HEAD, and remote
|
|
4. **Merge content** using bd's 3-way merge algorithm
|
|
5. **Reset to remote's history** (adopt their commit graph)
|
|
6. **Commit merged content** on top
|
|
|
|
This ensures sync never fails due to git merge conflicts - we handle merging at the JSONL content level where we have semantic understanding of the data.
|
|
|
|
## Changes
|
|
|
|
### `internal/syncbranch/worktree.go`
|
|
|
|
**New functions:**
|
|
- `getDivergence()` - Detects how many commits local/remote are ahead/behind
|
|
- `performContentMerge()` - Extracts and merges JSONL content from base/local/remote
|
|
- `performDeletionsMerge()` - Merges deletions.jsonl by union (keeps all deletions)
|
|
- `extractJSONLFromCommit()` - Extracts file content from a specific git commit
|
|
- `copyJSONLToMainRepo()` - Refactored helper for copying JSONL files
|
|
- `preemptiveFetchAndFastForward()` - Reduces divergence by fetching before commit
|
|
|
|
**Modified functions:**
|
|
- `PullFromSyncBranch()` - Now handles three cases:
|
|
- Already up-to-date: Remote has nothing new
|
|
- Fast-forward: Simple `--ff-only` merge
|
|
- **Diverged**: Content-based merge (the fix)
|
|
- `CommitToSyncBranch()` - Now fetches and fast-forwards before committing
|
|
|
|
**Enhanced structs:**
|
|
- `PullResult` - Added `Merged` and `FastForwarded` fields
|
|
|
|
### `cmd/bd/sync.go`
|
|
|
|
- Updated output messages to show merge type (fast-forward vs merged divergent histories)
|
|
|
|
### `internal/syncbranch/worktree_divergence_test.go` (new file)
|
|
|
|
Test coverage for:
|
|
- `getDivergence()` - 4 scenarios
|
|
- `extractJSONLFromCommit()` - 3 scenarios
|
|
- `performContentMerge()` - 2 scenarios
|
|
- `performDeletionsMerge()` - 2 scenarios
|
|
|
|
## How It Works
|
|
|
|
```
|
|
Clone A commits and pushes: origin/beads-sync = A -- B -- C
|
|
Clone B commits locally: local beads-sync = A -- B -- D
|
|
|
|
When Clone B syncs:
|
|
1. Fetch: gets C from origin
|
|
2. Detect divergence: local ahead 1, remote ahead 1
|
|
3. Find merge base: B
|
|
4. Extract: base=B's JSONL, local=D's JSONL, remote=C's JSONL
|
|
5. Content merge: merge JSONL using 3-way algorithm
|
|
6. Reset to origin: beads-sync = A -- B -- C
|
|
7. Commit merged: beads-sync = A -- B -- C -- M (merged content)
|
|
8. Push: no conflict, linear history
|
|
```
|
|
|
|
## Merge Rules
|
|
|
|
The 3-way merge uses these rules (from `internal/merge/merge.go`):
|
|
|
|
- **New issues**: Added from both sides
|
|
- **Deleted issues**: Deletion wins over modification
|
|
- **Modified issues**: Field-level merge
|
|
- `status`: "closed" always wins over "open"
|
|
- `updated_at`: Takes the max (latest)
|
|
- `closed_at`: Only set if status is "closed"
|
|
- `dependencies`: Union of both sides
|
|
- Other fields: Standard 3-way merge
|
|
|
|
## Edge Cases Handled
|
|
|
|
1. **Remote branch doesn't exist** - Nothing to pull, return early
|
|
2. **No common ancestor** - Use empty base for merge
|
|
3. **File doesn't exist in commit** - Use empty content
|
|
4. **Deletions.jsonl missing** - Non-fatal, skip deletion merge
|
|
5. **True conflicts** - Currently fails with error (manual resolution required)
|
|
|
|
## Future Improvements
|
|
|
|
### 1. Auto-Resolve All Conflicts (No Manual Resolution Required)
|
|
|
|
Currently, true conflicts (both sides changed same field to different values) fail the sync. This should be changed to auto-resolve deterministically:
|
|
|
|
| Field | Auto-Resolution Strategy |
|
|
|-------|-------------------------|
|
|
| `updated_at` | Already handled - takes max (latest) |
|
|
| `closed_at` | Already handled - takes max (latest) |
|
|
| `status` | Already handled - "closed" wins |
|
|
| `Priority` | Take higher priority (lower number = more urgent) |
|
|
| `IssueType` | Take left (local wins) |
|
|
| `Notes` | **Concatenate both** with separator (preserves all contributions) |
|
|
| `Title` | Take from side with latest `updated_at` on the issue |
|
|
| `Description` | Take from side with latest `updated_at` on the issue |
|
|
|
|
With this strategy, **no conflicts ever require manual resolution** - there's always a deterministic auto-resolution. The merge driver becomes fully automatic.
|
|
|
|
### 2. Auto-Push After Merge (Default Behavior)
|
|
|
|
Users shouldn't need to review merge diffs on beads metadata. The goal is "one command that just works":
|
|
|
|
```
|
|
bd sync # Should handle everything, including push
|
|
```
|
|
|
|
**Proposed behavior:**
|
|
- After successful content merge, auto-push by default
|
|
- Only hold off on push when unsafe conditions detected
|
|
|
|
**Safety checks before auto-push:**
|
|
1. No conflict markers in JSONL (shouldn't happen with full auto-resolve)
|
|
2. Issue count sanity check - didn't drop to zero unexpectedly
|
|
3. Reasonable deletion threshold - didn't delete > N% of issues in one sync
|
|
|
|
**The deletions manifest problem:**
|
|
- In multi-clone environments, deletions from one clone propagate to others
|
|
- This is correct behavior, but can feel like "corruption" when unexpected
|
|
- Swarms legitimately close/delete all issues sometimes
|
|
- Hard to distinguish "swarm finished all work" from "corruption"
|
|
|
|
**Proposed safeguards:**
|
|
- Track whether issues were *closed* (status change) vs *deleted* (removed from JSONL)
|
|
- Closing all issues = legitimate (swarm finished)
|
|
- Deleting all issues when there were many = suspicious, pause for confirmation
|
|
- Config option: `sync.auto_push` (default: true, can set to false for paranoid mode)
|
|
|
|
**Integration with bd doctor:**
|
|
- `bd doctor --fix` should also run this recovery logic
|
|
- But `bd doctor` is for daily/upgrade maintenance, not inner loop
|
|
- `bd sync` must handle divergence recovery itself
|
|
|
|
**The "one nuclear fix" philosophy:**
|
|
- `bd sync` should just work 99.9% of the time
|
|
- Auto-resolve all conflicts
|
|
- Auto-push when safe
|
|
- Only fail/pause when genuinely dangerous (mass deletion detected)
|
|
|
|
### 3. V1 Implementation Plan
|
|
|
|
Keep it simple for the first iteration:
|
|
|
|
**Auto-push behavior:**
|
|
1. After successful content merge, auto-push by default
|
|
2. One safety check: if issue count dropped by >50% AND there were >5 issues before, log a warning but still push
|
|
3. Config option `sync.require_confirmation_on_mass_delete` (default: false) for paranoid users who want to be prompted
|
|
|
|
**Rationale:**
|
|
- Logging gives forensics if something goes wrong
|
|
- Doesn't block the happy path (99.9% of syncs)
|
|
- Users who've been burned can enable confirmation mode
|
|
- We can tighten safeguards later based on real-world feedback
|
|
|
|
**What "mass deletion" means:**
|
|
- Issues that **vanished** from `issues.jsonl` (not just closed)
|
|
- `status=closed` is fine - swarm finished legitimately
|
|
- Issues disappearing entirely is suspicious
|
|
|
|
**Future safeguards (not v1):**
|
|
- Tombstone TTL: Ignore deletions older than N days
|
|
- Deletion rate limit: Pause if deletions.jsonl suddenly has 100+ new entries
|
|
- Protected issues: Certain issues can't be deleted via sync
|
|
|
|
---
|
|
|
|
## Summary of Work Items
|
|
|
|
1. **Already implemented (this PR):**
|
|
- Content-based merge for diverged histories
|
|
- Pre-emptive fetch before commit
|
|
- Deletions.jsonl merge
|
|
- Fast-forward detection
|
|
|
|
2. **Still to implement:**
|
|
- Auto-resolve all field conflicts (no manual resolution)
|
|
- Auto-push after merge with safety check
|
|
- Mass deletion warning/logging
|
|
- Config option for confirmation mode
|