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
+19 -26
View File
@@ -116,10 +116,8 @@ func (s *Store) StartDiscardTurn(ctx context.Context, taskID, sessionID, actor s
}
type TurnEnd struct {
Discard DiscardState
FinalizeBy string
MergeBy string
MergeWaiting bool
Discard DiscardState
FinalizeBy string
}
func (s *Store) EndTurn(ctx context.Context, sessionID, actor string, payload map[string]any, turn *api.Event, reviewRequest, finalizeMerge, continues bool) (TurnEnd, error) {
@@ -132,14 +130,10 @@ func (s *Store) EndTurn(ctx context.Context, sessionID, actor string, payload ma
state := DiscardNone
discardSession := ""
lead := false
mergeAfterTurn := false
othersRunning := false
err := tx.QueryRowContext(ctx, `
SELECT coalesce(t.id, ''), coalesce(t.project_id, ''), s.owner, s.role, coalesce(t.discard_state, ''), coalesce(t.discard_session, ''),
s.member = coalesce((SELECT f.member FROM agent_sessions f WHERE f.task_id = s.task_id AND f.role = s.role ORDER BY f.created_at LIMIT 1), ''),
coalesce(t.merge_after_turn, 0),
EXISTS (SELECT 1 FROM agent_sessions o WHERE o.task_id = s.task_id AND o.role = s.role AND o.state = ? AND o.id != s.id)
FROM agent_sessions s LEFT JOIN tasks t ON t.id = s.task_id WHERE s.id = ?`, string(api.AgentSessionStateRunning), sessionID).Scan(&taskID, &projectID, &owner, &role, &state, &discardSession, &lead, &mergeAfterTurn, &othersRunning)
s.member = coalesce((SELECT f.member FROM agent_sessions f WHERE f.task_id = s.task_id AND f.role = s.role ORDER BY f.created_at LIMIT 1), '')
FROM agent_sessions s LEFT JOIN tasks t ON t.id = s.task_id WHERE s.id = ?`, sessionID).Scan(&taskID, &projectID, &owner, &role, &state, &discardSession, &lead)
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
}
@@ -159,17 +153,20 @@ func (s *Store) EndTurn(ctx context.Context, sessionID, actor string, payload ma
return err
}
}
_, err = tx.ExecContext(ctx, `UPDATE agent_sessions SET state = ? WHERE id = ?`, string(api.AgentSessionStateIdle), sessionID)
if err != nil {
return err
}
if payload == nil {
payload = map[string]any{}
}
payload["state"] = string(api.AgentSessionStateIdle)
err = emit(sessionEvent("session.state", projectID, taskID, owner, sessionID, actor, payload))
if err != nil {
return err
handsOff := continues && role == string(api.Implementer) && state == DiscardNone
if !handsOff {
_, err = tx.ExecContext(ctx, `UPDATE agent_sessions SET state = ? WHERE id = ?`, string(api.AgentSessionStateIdle), sessionID)
if err != nil {
return err
}
if payload == nil {
payload = map[string]any{}
}
payload["state"] = string(api.AgentSessionStateIdle)
err = emit(sessionEvent("session.state", projectID, taskID, owner, sessionID, actor, payload))
if err != nil {
return err
}
}
if role != string(api.Implementer) {
@@ -181,12 +178,8 @@ func (s *Store) EndTurn(ctx context.Context, sessionID, actor string, payload ma
case state == DiscardRunning && discardSession == sessionID:
end.Discard = DiscardClosing
_, err = tx.ExecContext(ctx, `UPDATE tasks SET discard_state = ? WHERE id = ?`, string(DiscardClosing), taskID)
case mergeAfterTurn && (othersRunning || continues):
end.MergeWaiting = true
case mergeAfterTurn:
err = tx.QueryRowContext(ctx, `SELECT merging_by FROM tasks WHERE id = ?`, taskID).Scan(&end.MergeBy)
case state == DiscardNone && finalizeMerge && lead:
err = tx.QueryRowContext(ctx, `UPDATE tasks SET finalizing = merging_by, merging_by = '' WHERE id = ? AND merging_by != '' RETURNING finalizing`, taskID).Scan(&end.FinalizeBy)
err = tx.QueryRowContext(ctx, `UPDATE tasks SET finalizing = merging_by, merging_by = '' WHERE id = ? AND merging_by != '' AND merge_after_turn = 0 RETURNING finalizing`, taskID).Scan(&end.FinalizeBy)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}