Refuse turns, discard and subtasks while a merge is finalizing

This commit is contained in:
Ian Gulliver
2026-10-16 12:46:28 -07:00
parent 0e52253d07
commit ecfae97d97
11 changed files with 287 additions and 233 deletions
+1 -5
View File
@@ -531,11 +531,7 @@ func (m *Manager) paneFinished(ctx context.Context, f api.Focus) (string, error)
if task.Status != api.TaskStatusActive {
return "is " + string(task.Status), nil
}
mergingBy, err := m.store.MergingBy(ctx, task.Id)
if err != nil {
return "", err
}
if mergingBy != "" {
if task.Merging {
return "is merging", nil
}
if f.Tab != api.TabInbox {
+13 -7
View File
@@ -283,6 +283,11 @@ func (m *Manager) Recover(ctx context.Context) error {
return err
}
err = m.recoverFinalizing(ctx)
if err != nil {
return err
}
staged, err := m.recoverStages(ctx)
if err != nil {
return err
@@ -365,7 +370,7 @@ func (m *Manager) failResume(ctx context.Context, sess api.AgentSession, task ap
if derr != nil {
return derr
}
next, serr := m.store.EndTurn(ctx, sess.Id, "byre", map[string]any{"error": err.Error()}, nil, false)
next, _, serr := m.store.EndTurn(ctx, sess.Id, "byre", map[string]any{"error": err.Error()}, nil, false, false)
if serr != nil {
return serr
}
@@ -694,7 +699,7 @@ func (m *Manager) pump(l *live) {
slog.Error("drop stage output", "session", l.id, "err", err)
}
}
next, err := m.store.EndTurn(ctx, l.id, agentActor, map[string]any{"error": "agent process exited mid-turn"}, nil, false)
next, _, err := m.store.EndTurn(ctx, l.id, agentActor, map[string]any{"error": "agent process exited mid-turn"}, nil, false, false)
if err != nil {
slog.Error("record session exit", "session", l.id, "err", err)
}
@@ -930,7 +935,7 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
}
turn := m.event(l, agentActor, "turn.ended", payload)
next, err := m.store.EndTurn(ctx, l.id, agentActor, nil, &turn, review)
next, finalizeBy, err := m.store.EndTurn(ctx, l.id, agentActor, nil, &turn, review, finished && !handoff)
if err != nil {
return err
}
@@ -957,12 +962,13 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
go m.continueFromHandoff(l.id, res.result)
return nil
}
if finalizeBy != "" {
m.logUndelivered(ctx, l.id, "turn ended in merge")
go m.finishMerge(l, finalizeBy)
return nil
}
mergeBy := m.takeMerge(l.taskID)
switch {
case mergeBy != "" && finished:
m.logUndelivered(ctx, l.id, "turn ended in merge")
go m.finishMerge(l, mergeBy)
return nil
case mergeBy != "":
go m.releaseMerge(l.projectID, l.taskID)
if !res.isError || interrupted {
+25
View File
@@ -135,6 +135,27 @@ func (m *Manager) recoverMerges(ctx context.Context) error {
return nil
}
func (m *Manager) recoverFinalizing(ctx context.Context) error {
finalizing, err := m.store.FinalizingTasks(ctx)
if err != nil {
return err
}
for _, t := range finalizing {
err = m.store.ClearFinalizing(ctx, t.Id)
if err != nil {
return err
}
sessions, err := m.store.ListSessions(ctx, t.Id)
if err != nil {
return err
}
slog.Error("merge was finishing at startup", "task", t.Id)
m.fileTaskError(ctx, t, sessions, "byre restarted while finishing this task's merge, so it may not have been pushed, or the task's worktree may be gone. Check the default branch before merging again.")
}
return nil
}
func (m *Manager) startHeldMerges(ctx context.Context) {
projects, err := m.store.ProjectsWithQueuedMerges(ctx)
if err != nil {
@@ -253,6 +274,10 @@ func (m *Manager) finishMerge(l *live, login string) {
}
slog.Error("finish merge", "task", l.taskID, "err", err)
cerr := m.store.ClearFinalizing(ctx, l.taskID)
if cerr != nil {
slog.Error("clear merge finalizing", "task", l.taskID, "err", cerr)
}
m.fileError(l, "Merging the task failed: "+err.Error())
err = m.Deliver(ctx, l.id)
if err != nil {
+2 -6
View File
@@ -320,11 +320,7 @@ func (m *Manager) MergeIntoParent(ctx context.Context, task api.Task, user api.U
if found && sess.State == api.AgentSessionStateRunning {
return api.Task{}, fmt.Errorf("%w: subtask %s's agent is mid-turn; wait for the turn to end or interrupt it", ErrConflict, task.Id)
}
mergingBy, err := m.store.MergingBy(ctx, task.Id)
if err != nil {
return api.Task{}, err
}
if mergingBy != "" || task.MergeQueuedAt != nil {
if task.Merging || task.MergeQueuedAt != nil {
return api.Task{}, fmt.Errorf("%w: subtask %s is merging into the default branch", ErrConflict, task.Id)
}
@@ -332,7 +328,7 @@ func (m *Manager) MergeIntoParent(ctx context.Context, task api.Task, user api.U
switch {
case errors.Is(err, store.ErrNotFound):
return api.Task{}, fmt.Errorf("%w: subtask %s is closed", ErrConflict, task.Id)
case errors.Is(err, store.ErrDiscardPending), errors.Is(err, store.ErrParentMergeRequested):
case errors.Is(err, store.ErrDiscardPending), errors.Is(err, store.ErrMerging), errors.Is(err, store.ErrParentMergeRequested):
return api.Task{}, fmt.Errorf("%w: subtask %s: %w", ErrConflict, task.Id, err)
case err != nil:
return api.Task{}, err