Claim a held merge as soon as the merge ahead of it finishes

This commit is contained in:
Ian Gulliver
2026-10-21 09:02:45 -07:00
parent a130b9556d
commit cdfc148ba9
6 changed files with 205 additions and 36 deletions
+34 -11
View File
@@ -470,6 +470,7 @@ func (m *Manager) Recover(ctx context.Context) error {
go m.deliverAllLeads()
go m.deliverAllQueued(context.Background())
go m.advanceMergeClaims(context.Background())
go m.startHeldMerges(context.Background())
go m.recoverMergeAfterReviews(context.Background())
go m.auditAll(context.Background())
@@ -484,15 +485,18 @@ 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, false)
end, serr := m.store.EndTurn(ctx, sess.Id, "byre", map[string]any{"error": err.Error()}, nil, false, false, false)
if serr != nil {
return serr
}
switch {
case next == store.DiscardClosing:
case end.Discard == store.DiscardClosing:
go m.finishDiscard(task.Id)
case next == store.DiscardHeld:
case end.Discard == store.DiscardHeld:
go m.startDiscard(task.Id)
case end.MergeBy != "":
go m.startClaimedMerge(task.Id, end.MergeBy)
case end.MergeWaiting:
case sess.Role == api.Implementer && m.leadsTask(ctx, task.Id, sess.Id):
m.abandonMerge(task.Id, task.ProjectId)
}
@@ -679,6 +683,18 @@ func (m *Manager) failStart(ctx context.Context, sessionID, actor string, err er
if serr != nil {
slog.Error("record failed start", "session", sessionID, "err", serr)
}
go m.advanceSessionMergeClaim(context.Background(), sessionID)
}
func (m *Manager) advanceSessionMergeClaim(ctx context.Context, sessionID string) {
sess, err := m.store.GetSession(ctx, sessionID)
if err != nil {
slog.Error("load session for merge claim", "session", sessionID, "err", err)
return
}
if sess.TaskId != "" {
m.advanceMergeClaim(sess.TaskId)
}
}
func (m *Manager) syncModel(ctx context.Context, l *live, model string) error {
@@ -998,7 +1014,7 @@ func (m *Manager) pump(l *live) {
}
}
m.endMergeTurn(l.id, true)
next, _, err := m.store.EndTurn(ctx, l.id, agentActor, map[string]any{"error": "agent process exited mid-turn", "stopped": true}, nil, false, false)
end, err := m.store.EndTurn(ctx, l.id, agentActor, map[string]any{"error": "agent process exited mid-turn", "stopped": true}, nil, false, false, false)
if err != nil {
slog.Error("record session exit", "session", l.id, "err", err)
}
@@ -1006,7 +1022,7 @@ func (m *Manager) pump(l *live) {
m.finishReviewer(l, "the reviewer's process exited mid-turn")
return
}
switch next {
switch end.Discard {
case store.DiscardClosing:
m.finishDiscard(l.taskID)
return
@@ -1014,8 +1030,11 @@ func (m *Manager) pump(l *live) {
m.startDiscard(l.taskID)
return
}
if end.MergeBy != "" {
go m.startClaimedMerge(l.taskID, end.MergeBy)
}
if l.owner == "" {
if m.leadsTask(ctx, l.taskID, l.id) {
if end.MergeBy == "" && !end.MergeWaiting && m.leadsTask(ctx, l.taskID, l.id) {
m.abandonMerge(l.taskID, l.projectID)
}
m.recordError(l, "The agent process exited mid-turn. Send a message to resume it.")
@@ -1310,15 +1329,19 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
state = map[string]any{"stopped": true}
}
turn := m.event(l, agentActor, "turn.ended", payload)
next, finalizeBy, err := m.store.EndTurn(ctx, l.id, agentActor, state, &turn, review && !held, finished && !handoff)
end, err := m.store.EndTurn(ctx, l.id, agentActor, state, &turn, review && !held, finished && !handoff, handoff)
if err != nil {
return err
}
if snapshot && !held {
m.snapshotSoon(liveTarget(l), api.SnapshotTriggerTurnEnd, 0, review, false)
}
if end.MergeBy != "" {
go m.startClaimedMerge(l.taskID, end.MergeBy)
return nil
}
switch next {
switch end.Discard {
case store.DiscardClosing:
m.logUndelivered(ctx, l.id, "turn ended in discard")
go m.finishDiscard(l.taskID)
@@ -1337,14 +1360,14 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
go m.continueFromHandoff(l.id, res.result)
return nil
}
if finalizeBy != "" {
if end.FinalizeBy != "" {
m.logUndelivered(ctx, l.id, "turn ended in merge")
keep = owner
go m.finishMerge(l, finalizeBy)
go m.finishMerge(l, end.FinalizeBy)
return nil
}
mergeBy := ""
if m.leadsTask(ctx, l.taskID, l.id) {
if !end.MergeWaiting && m.leadsTask(ctx, l.taskID, l.id) {
mergeBy = m.takeMerge(l.taskID)
}
switch {