Run the merge queue on a state-driven coordinator

This commit is contained in:
Ian Gulliver
2026-10-21 11:28:07 -07:00
parent a971cce9d6
commit 7fbdf1669a
13 changed files with 509 additions and 561 deletions
+25 -67
View File
@@ -74,8 +74,7 @@ type Manager struct {
snapMu sync.Mutex
snaps map[string]*snapState
mergeMu sync.Mutex
merges map[string]*projectMerge
pokes chan struct{}
reviewMu sync.Mutex
@@ -169,7 +168,7 @@ func New(st *store.Store, worktreeRoot, leadRoot, handsRoot, codexHome, antigrav
landing: map[string]landing{},
signals: make(chan []api.Event, 1024),
snaps: map[string]*snapState{},
merges: map[string]*projectMerge{},
pokes: make(chan struct{}, 1),
previewListeners: map[int]*previewListener{},
bad: newBadLines(),
watchStop: make(chan struct{}),
@@ -236,9 +235,15 @@ func (m *Manager) newSession(ctx context.Context, task api.Task, creator api.Use
}
}
sess := sessionFor(task, creator, member, model)
err = m.store.CreateSession(ctx, sess, task.ProjectId)
return sess, err
}
func sessionFor(task api.Task, creator api.User, member, model string) api.AgentSession {
id := ids.New(ids.Session)
vendor := vendorOf(model)
sess := api.AgentSession{
return api.AgentSession{
Id: id,
TaskId: task.Id,
Vendor: vendor,
@@ -250,9 +255,6 @@ func (m *Manager) newSession(ctx context.Context, task api.Task, creator api.Use
CreatedBy: creator.Login,
CreatedAt: time.Now().UTC(),
}
err = m.store.CreateSession(ctx, sess, task.ProjectId)
return sess, err
}
func (m *Manager) Send(ctx context.Context, sessionID, actor, text, model, sendID, client string, images ...string) (api.AgentSession, error) {
@@ -354,12 +356,7 @@ func (m *Manager) stopLive(sessionID string) {
}
func (m *Manager) Recover(ctx context.Context) error {
err := m.recoverMerges(ctx)
if err != nil {
return err
}
err = m.recoverFinalizing(ctx)
err := m.store.ClearMainPushes(ctx)
if err != nil {
return err
}
@@ -470,8 +467,8 @@ 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.coordinate()
m.poke()
go m.recoverMergeAfterReviews(context.Background())
go m.auditAll(context.Background())
return nil
@@ -489,16 +486,11 @@ func (m *Manager) failResume(ctx context.Context, sess api.AgentSession, task ap
if serr != nil {
return serr
}
switch {
case end.Discard == store.DiscardClosing:
switch end.Discard {
case store.DiscardClosing:
go m.finishDiscard(task.Id)
case end.Discard == store.DiscardHeld:
case store.DiscardHeld:
go m.startDiscard(task.Id)
case end.MergeBy != "":
go m.advanceMergeClaim(task.Id)
case end.MergeWaiting:
case sess.Role == api.Implementer && m.leadsTask(ctx, task.Id, sess.Id):
m.abandonMerge(task.Id, task.ProjectId)
}
return nil
}
@@ -683,18 +675,6 @@ 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 {
@@ -1031,13 +1011,7 @@ func (m *Manager) pump(l *live) {
m.startDiscard(l.taskID)
return
}
if end.MergeBy != "" {
go m.advanceMergeClaim(l.taskID)
}
if l.owner == "" {
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.")
}
}
@@ -1223,13 +1197,8 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
payload["member"] = l.member
}
owner := m.ownsTask(l.id)
keep := false
if owner {
defer func() {
if !keep {
m.endMergeTurn(l.id, false)
}
}()
defer m.endMergeTurn(l.id, false)
}
if (owner || m.landingBy(l.taskID, l.id) != "") && !finished {
err := git.AbortMerge(l.dir)
@@ -1337,10 +1306,6 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
if snapshot && !held {
m.snapshotSoon(liveTarget(l), api.SnapshotTriggerTurnEnd, 0, review, false)
}
if end.MergeBy != "" {
go m.advanceMergeClaim(l.taskID)
return nil
}
switch end.Discard {
case store.DiscardClosing:
@@ -1348,7 +1313,6 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
go m.finishDiscard(l.taskID)
return nil
case store.DiscardHeld:
go m.nextMerge(l.projectID)
if stageFinished {
go m.advanceStage(l)
return nil
@@ -1363,23 +1327,8 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
}
if end.FinalizeBy != "" {
m.logUndelivered(ctx, l.id, "turn ended in merge")
keep = owner
go m.finishMerge(l, end.FinalizeBy)
return nil
}
mergeBy := ""
if !end.MergeWaiting && m.leadsTask(ctx, l.taskID, l.id) {
mergeBy = m.takeMerge(l.taskID)
}
switch {
case mergeBy != "":
go m.releaseMerge(l.projectID, l.taskID)
if !res.isError || interrupted {
m.recordError(l, "The Merge turn ended unfinished (interrupted, or with no final message), so byre didn't squash or push the task. Merge again to retry.")
}
default:
go m.nextMerge(l.projectID)
}
if stageFinished {
go m.advanceStage(l)
return nil
@@ -1398,6 +1347,15 @@ func (m *Manager) afterTurn(ctx context.Context, l *live) {
m.startDiscard(l.taskID)
return
}
task, err := m.store.GetTask(ctx, l.taskID)
if err != nil {
slog.Error("read task after turn", "task", l.taskID, "err", err)
return
}
if task.Merging {
m.logUndelivered(ctx, l.id, "task is merging")
return
}
m.deliverTeam(ctx, l)
if m.interrupted(l.id) {
m.logUndelivered(ctx, l.id, "turn was interrupted")