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
+23 -9
View File
@@ -115,9 +115,15 @@ func (s *Store) StartDiscardTurn(ctx context.Context, taskID, sessionID, actor s
})
}
func (s *Store) EndTurn(ctx context.Context, sessionID, actor string, payload map[string]any, turn *api.Event, reviewRequest, finalizeMerge bool) (DiscardState, string, error) {
next := DiscardNone
finalizing := ""
type TurnEnd struct {
Discard DiscardState
FinalizeBy string
MergeBy string
MergeWaiting bool
}
func (s *Store) EndTurn(ctx context.Context, sessionID, actor string, payload map[string]any, turn *api.Event, reviewRequest, finalizeMerge, continues bool) (TurnEnd, error) {
end := TurnEnd{Discard: DiscardNone}
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
taskID := ""
projectID := ""
@@ -126,10 +132,14 @@ 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), '')
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)
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)
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
}
@@ -167,19 +177,23 @@ func (s *Store) EndTurn(ctx context.Context, sessionID, actor string, payload ma
}
switch {
case state == DiscardHeld:
next = DiscardHeld
end.Discard = DiscardHeld
case state == DiscardRunning && discardSession == sessionID:
next = DiscardClosing
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, `UPDATE tasks SET merge_after_turn = 0 WHERE id = ? RETURNING merging_by`, 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(&finalizing)
err = tx.QueryRowContext(ctx, `UPDATE tasks SET finalizing = merging_by, merging_by = '' WHERE id = ? AND merging_by != '' RETURNING finalizing`, taskID).Scan(&end.FinalizeBy)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
}
return err
})
return next, finalizing, err
return end, err
}
func (s *Store) CloseDiscard(ctx context.Context, taskID string) error {
@@ -0,0 +1 @@
ALTER TABLE tasks ADD COLUMN merge_after_turn INTEGER NOT NULL DEFAULT 0;
+67 -1
View File
@@ -496,7 +496,7 @@ func (s *Store) TakeMergingBy(ctx context.Context, id string) (string, error) {
if login == "" {
return nil
}
_, err = tx.ExecContext(ctx, `UPDATE tasks SET merging_by = '' WHERE id = ?`, id)
_, err = tx.ExecContext(ctx, `UPDATE tasks SET merging_by = '', merge_after_turn = 0 WHERE id = ?`, id)
if err != nil {
return err
}
@@ -712,6 +712,72 @@ func (s *Store) TakeQueuedMerge(ctx context.Context, id string) (string, error)
return login, err
}
func (s *Store) ClaimQueuedMerge(ctx context.Context, id string) (string, bool, error) {
login := ""
claimed := false
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
projectID := ""
err := tx.QueryRowContext(ctx, `SELECT project_id, merge_queued_by, EXISTS (SELECT 1 FROM agent_sessions s WHERE s.task_id = tasks.id AND s.role = ? AND s.state = ?) FROM tasks WHERE id = ?`,
string(api.Implementer), string(api.AgentSessionStateRunning), id).Scan(&projectID, &login, &claimed)
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 || !claimed {
return err
}
_, err = tx.ExecContext(ctx, `UPDATE tasks SET merging_by = ?, merge_after_turn = 1 WHERE id = ?`, login, id)
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}})
})
return login, claimed, err
}
func (s *Store) TakeIdleMergeClaim(ctx context.Context, id string) (string, error) {
login := ""
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
err := tx.QueryRowContext(ctx, `UPDATE tasks SET merge_after_turn = 0 WHERE id = ? AND merge_after_turn = 1
AND NOT EXISTS (SELECT 1 FROM agent_sessions s WHERE s.task_id = tasks.id AND s.role = ? AND s.state = ?) RETURNING merging_by`,
id, string(api.Implementer), string(api.AgentSessionStateRunning)).Scan(&login)
if errors.Is(err, sql.ErrNoRows) {
return nil
}
return err
})
return login, 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