Run the merge queue on a state-driven coordinator
This commit is contained in:
+25
-67
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user