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
+34 -156
View File
@@ -459,20 +459,6 @@ func (s *Store) SetTitle(ctx context.Context, id, title, actor string) error {
})
}
func (s *Store) SetMergingBy(ctx context.Context, id, login string) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
projectID := ""
err := tx.QueryRowContext(ctx, `UPDATE tasks SET merging_by = ? WHERE id = ? AND discard_state = '' AND finalizing = '' RETURNING project_id`, login, id).Scan(&projectID)
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
}
if err != nil {
return err
}
return emit(api.Event{Kind: "task.merge_started", ProjectId: &projectID, TaskId: &id, Actor: login, Payload: map[string]any{"id": id}})
})
}
func (s *Store) MergingBy(ctx context.Context, id string) (string, error) {
login := ""
err := s.db.QueryRowContext(ctx, `SELECT merging_by FROM tasks WHERE id = ?`, id).Scan(&login)
@@ -545,61 +531,6 @@ func (s *Store) setFinalizing(ctx context.Context, id, commit, notes string, unc
})
}
type FinalizingMerge struct {
Task api.Task
By string
Commit string
Notes string
Unchanged bool
}
func (s *Store) FinalizingMerges(ctx context.Context) ([]FinalizingMerge, error) {
rows, err := s.db.QueryContext(ctx, `SELECT `+taskColumns+`, finalizing, finalizing_commit, finalizing_notes, finalizing_unchanged FROM tasks WHERE status = ? AND finalizing != ''`, string(api.TaskStatusActive))
if err != nil {
return nil, err
}
defer rows.Close()
merges := []FinalizingMerge{}
for rows.Next() {
f := FinalizingMerge{}
f.Task, err = scanTask(trailingScanner{rows, []any{&f.By, &f.Commit, &f.Notes, &f.Unchanged}})
if err != nil {
return nil, err
}
merges = append(merges, f)
}
return merges, rows.Err()
}
func (s *Store) MergingTasks(ctx context.Context) ([]api.Task, error) {
return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE status = ? AND merging_by != ''`, string(api.TaskStatusActive))
}
func (s *Store) QueuedMerges(ctx context.Context, projectID string) ([]api.Task, error) {
return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE project_id = ? AND status = ? AND merge_queued_by != '' ORDER BY merge_queued_at`,
projectID, string(api.TaskStatusActive))
}
func (s *Store) ProjectsWithQueuedMerges(ctx context.Context) ([]string, error) {
rows, err := s.db.QueryContext(ctx, `SELECT DISTINCT project_id FROM tasks WHERE status = ? AND merge_queued_by != ''`, string(api.TaskStatusActive))
if err != nil {
return nil, err
}
defer rows.Close()
ids := []string{}
for rows.Next() {
id := ""
err = rows.Scan(&id)
if err != nil {
return nil, err
}
ids = append(ids, id)
}
return ids, rows.Err()
}
func (s *Store) queryTasks(ctx context.Context, query string, args ...any) ([]api.Task, error) {
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
@@ -674,21 +605,6 @@ func (s *Store) ParentMergeBy(ctx context.Context, id string) (string, error) {
return login, err
}
func (s *Store) QueueMerge(ctx context.Context, id, login string) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
projectID := ""
err := tx.QueryRowContext(ctx, `UPDATE tasks SET merge_queued_by = ?, merge_queued_at = ? WHERE id = ? AND status = ? AND merge_queued_by = '' AND discard_state = '' RETURNING project_id`,
login, now(), id, string(api.TaskStatusActive)).Scan(&projectID)
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
}
if err != nil {
return err
}
return emit(api.Event{Kind: "task.merge_queued", ProjectId: &projectID, TaskId: &id, Actor: login, Payload: map[string]any{"id": id}})
})
}
func (s *Store) TakeQueuedMerge(ctx context.Context, id string) (string, error) {
login := ""
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
@@ -712,54 +628,6 @@ func (s *Store) TakeQueuedMerge(ctx context.Context, id string) (string, error)
return login, err
}
func (s *Store) ClaimQueuedMerge(ctx context.Context, id string) (string, error) {
login := ""
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
projectID := ""
err := tx.QueryRowContext(ctx, `SELECT project_id, merge_queued_by FROM tasks WHERE id = ?`, id).Scan(&projectID, &login)
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
}
if err != nil {
return err
}
if login == "" {
return nil
}
_, err = tx.ExecContext(ctx, `UPDATE tasks SET merge_queued_by = '', merge_queued_at = '' WHERE id = ?`, id)
if err != nil {
return err
}
err = emit(api.Event{Kind: "task.merge_unqueued", ProjectId: &projectID, TaskId: &id, Actor: "byre", Payload: map[string]any{"id": id}})
if err != nil {
return err
}
res, err := tx.ExecContext(ctx, `UPDATE tasks SET merging_by = ?, merge_after_turn = 1 WHERE id = ? AND discard_state = '' AND finalizing = ''`, login, id)
if err != nil {
return err
}
n, err := res.RowsAffected()
if err != nil {
return err
}
if n == 0 {
login = ""
return nil
}
return emit(api.Event{Kind: "task.merge_started", ProjectId: &projectID, TaskId: &id, Actor: login, Payload: map[string]any{"id": id}})
})
return login, err
}
func (s *Store) MergeClaim(ctx context.Context, id string) (string, error) {
login := ""
err := s.db.QueryRowContext(ctx, `SELECT merging_by FROM tasks WHERE id = ? AND merge_after_turn = 1`, id).Scan(&login)
if errors.Is(err, sql.ErrNoRows) {
return "", nil
}
return login, err
}
func (s *Store) ReserveMergeTurn(ctx context.Context, id, sessionID string) (bool, error) {
reserved := false
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
@@ -784,25 +652,6 @@ func (s *Store) ReserveMergeTurn(ctx context.Context, id, sessionID string) (boo
return reserved, err
}
func (s *Store) MergeClaims(ctx context.Context) ([]string, error) {
rows, err := s.db.QueryContext(ctx, `SELECT id FROM tasks WHERE status = ? AND merge_after_turn = 1`, string(api.TaskStatusActive))
if err != nil {
return nil, err
}
defer rows.Close()
ids := []string{}
for rows.Next() {
id := ""
err = rows.Scan(&id)
if err != nil {
return nil, err
}
ids = append(ids, id)
}
return ids, rows.Err()
}
type MergeAfterReview struct {
By string
ReviewID string
@@ -1335,21 +1184,50 @@ func (s *Store) Broadcast(ev api.Event) {
}
func (s *Store) CreateSession(ctx context.Context, sess api.AgentSession, projectID string) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
return insertSession(ctx, tx, emit, sess, projectID)
})
}
func insertSession(ctx context.Context, tx *sql.Tx, emit emitFunc, sess api.AgentSession, projectID string) error {
forkedFrom := ""
forkedAt := ""
if sess.ForkedFrom != nil && sess.ForkedAt != nil {
forkedFrom = *sess.ForkedFrom
forkedAt = *sess.ForkedAt
}
_, err := tx.ExecContext(ctx, `
INSERT INTO agent_sessions (id, task_id, owner, vendor, model, vendor_session_id, state, role, member, created_by, created_at, forked_from, forked_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, sess.Id, nullable(sess.TaskId), sess.Owner, string(sess.Vendor), sess.Model, sess.VendorSessionId, string(sess.State), string(sess.Role), sess.Member, sess.CreatedBy, formatTime(sess.CreatedAt), forkedFrom, forkedAt)
if err != nil {
return err
}
return emit(sessionEvent("session.created", projectID, sess.TaskId, sess.Owner, sess.Id, sess.CreatedBy, ToPayload(sess)))
}
func (s *Store) ReplaceSession(ctx context.Context, oldID string, sess api.AgentSession, projectID, actor string) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO agent_sessions (id, task_id, owner, vendor, model, vendor_session_id, state, role, member, created_by, created_at, forked_from, forked_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, sess.Id, nullable(sess.TaskId), sess.Owner, string(sess.Vendor), sess.Model, sess.VendorSessionId, string(sess.State), string(sess.Role), sess.Member, sess.CreatedBy, formatTime(sess.CreatedAt), forkedFrom, forkedAt)
status, discard := "", ""
err := tx.QueryRowContext(ctx, `SELECT status, discard_state FROM tasks WHERE id = ?`, sess.TaskId).Scan(&status, &discard)
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
}
if err != nil {
return err
}
return emit(sessionEvent("session.created", projectID, sess.TaskId, sess.Owner, sess.Id, sess.CreatedBy, ToPayload(sess)))
if status != string(api.TaskStatusActive) {
return fmt.Errorf("%w: task %s is %s", ErrNotFound, sess.TaskId, status)
}
if discard != "" {
return fmt.Errorf("%w: task %s is being discarded", ErrStateChanged, sess.TaskId)
}
running := api.AgentSessionStateRunning
err = transitionSessionTx(ctx, tx, emit, oldID, &running, api.AgentSessionStateCompleted, actor, nil)
if err != nil {
return err
}
return insertSession(ctx, tx, emit, sess, projectID)
})
}