Skip to content

Commit 9a9974e

Browse files
furiosaclaude
andcommitted
fix(refinery): serialize all pushes to main via merge slot
Add merge slot acquisition in doMerge() before pushing to origin/main. This prevents a race condition where the normal merge path and a conflict-resolution polecat can push to main concurrently, causing non-fast-forward errors. The fix: - Acquires the merge slot before pushing (non-blocking) - If slot is held by another process, returns failure so MR retries later - Releases the slot after push completes (success or failure) via defer This ensures all pushes to main are serialized, regardless of whether they come from the normal merge queue or conflict resolution. Fixes: gastownhallgh-594 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e9ed08e commit 9a9974e

1 file changed

Lines changed: 38 additions & 0 deletions

File tree

internal/refinery/engineer.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,44 @@ func (e *Engineer) doMerge(ctx context.Context, branch, target, sourceIssue stri
352352
}
353353

354354
// Step 7: Push to origin
355+
// === MERGE SLOT GATE: Serialize all pushes to main ===
356+
// Acquire the merge slot to prevent racing with conflict-resolution polecats.
357+
// This fixes the race condition where both the normal merge path and a
358+
// conflict-resolution polecat can push to main concurrently (gh-594).
359+
holder := e.rig.Name + "/refinery"
360+
_, slotErr := e.beads.MergeSlotEnsureExists()
361+
if slotErr != nil {
362+
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: could not ensure merge slot: %v\n", slotErr)
363+
// Continue anyway - slot is optional for backwards compatibility
364+
}
365+
slotStatus, slotErr := e.beads.MergeSlotAcquire(holder, false)
366+
if slotErr != nil {
367+
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: could not acquire merge slot: %v\n", slotErr)
368+
// Continue anyway - slot is optional for backwards compatibility
369+
} else if !slotStatus.Available && slotStatus.Holder != "" && slotStatus.Holder != holder {
370+
// Slot is held by someone else (likely a conflict-resolution polecat)
371+
// Return failure so MR stays in queue and retries after slot is released
372+
_, _ = fmt.Fprintf(e.output, "[Engineer] Merge slot held by %s - deferring push\n", slotStatus.Holder)
373+
return ProcessResult{
374+
Success: false,
375+
Error: fmt.Sprintf("merge slot held by %s - will retry when released", slotStatus.Holder),
376+
}
377+
} else {
378+
_, _ = fmt.Fprintf(e.output, "[Engineer] Acquired merge slot for push\n")
379+
}
380+
// Release the slot after push completes (success or failure)
381+
defer func() {
382+
if releaseErr := e.beads.MergeSlotRelease(holder); releaseErr != nil {
383+
// Only log if it's a real error (not "slot not held")
384+
errStr := releaseErr.Error()
385+
if !strings.Contains(errStr, "not held") && !strings.Contains(errStr, "not found") {
386+
_, _ = fmt.Fprintf(e.output, "[Engineer] Warning: failed to release merge slot: %v\n", releaseErr)
387+
}
388+
} else {
389+
_, _ = fmt.Fprintf(e.output, "[Engineer] Released merge slot\n")
390+
}
391+
}()
392+
355393
_, _ = fmt.Fprintf(e.output, "[Engineer] Pushing to origin/%s...\n", target)
356394
if err := e.git.Push("origin", target, false); err != nil {
357395
return ProcessResult{

0 commit comments

Comments
 (0)