Refuse turns, discard and subtasks while a merge is finalizing
This commit is contained in:
@@ -23,7 +23,7 @@ var (
|
||||
ErrMerging = errors.New("the task is merging")
|
||||
ErrMergingIntoParent = errors.New("the task is merging into its parent task")
|
||||
ErrHasSubtasks = errors.New("the task has active subtasks")
|
||||
ErrParentNotOpen = errors.New("the parent task is closed or being discarded")
|
||||
ErrParentNotOpen = errors.New("the parent task is closed, being discarded or finishing its merge")
|
||||
ErrParentMergeRequested = errors.New("the subtask is already merging into its parent")
|
||||
)
|
||||
|
||||
@@ -48,12 +48,13 @@ func (s *Store) RequestDiscard(ctx context.Context, taskID, actor string) (Disca
|
||||
projectID := ""
|
||||
status := ""
|
||||
mergingBy := ""
|
||||
finalizing := ""
|
||||
parentMergeBy := ""
|
||||
subtasks := 0
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT project_id, status, merging_by, parent_merge_by, discard_state,
|
||||
SELECT project_id, status, merging_by, finalizing, parent_merge_by, discard_state,
|
||||
(SELECT count(*) FROM tasks AS sub WHERE sub.parent_task_id = tasks.id AND sub.status = ?)
|
||||
FROM tasks WHERE id = ?`, string(api.TaskStatusActive), taskID).Scan(&projectID, &status, &mergingBy, &parentMergeBy, &state, &subtasks)
|
||||
FROM tasks WHERE id = ?`, string(api.TaskStatusActive), taskID).Scan(&projectID, &status, &mergingBy, &finalizing, &parentMergeBy, &state, &subtasks)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ErrNotFound
|
||||
}
|
||||
@@ -67,7 +68,7 @@ func (s *Store) RequestDiscard(ctx context.Context, taskID, actor string) (Disca
|
||||
return nil
|
||||
case state != DiscardNone:
|
||||
return ErrDiscardPending
|
||||
case mergingBy != "":
|
||||
case mergingBy != "" || finalizing != "":
|
||||
return ErrMerging
|
||||
case parentMergeBy != "":
|
||||
return ErrMergingIntoParent
|
||||
@@ -114,8 +115,9 @@ 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 bool) (DiscardState, error) {
|
||||
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 := ""
|
||||
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
taskID := ""
|
||||
projectID := ""
|
||||
@@ -167,10 +169,15 @@ func (s *Store) EndTurn(ctx context.Context, sessionID, actor string, payload ma
|
||||
case state == DiscardRunning && discardSession == sessionID:
|
||||
next = DiscardClosing
|
||||
_, err = tx.ExecContext(ctx, `UPDATE tasks SET discard_state = ? WHERE id = ?`, string(DiscardClosing), taskID)
|
||||
case state == DiscardNone && finalizeMerge:
|
||||
err = tx.QueryRowContext(ctx, `UPDATE tasks SET finalizing = merging_by, merging_by = '' WHERE id = ? AND merging_by != '' RETURNING finalizing`, taskID).Scan(&finalizing)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
})
|
||||
return next, err
|
||||
return next, finalizing, err
|
||||
}
|
||||
|
||||
func (s *Store) CloseDiscard(ctx context.Context, taskID string) error {
|
||||
|
||||
+37
-11
@@ -446,6 +446,8 @@ CREATE TABLE summary_reads (
|
||||
seq INTEGER NOT NULL,
|
||||
PRIMARY KEY (login, task_id)
|
||||
);
|
||||
`}, {"add-task-finalizing", `
|
||||
ALTER TABLE tasks ADD COLUMN finalizing TEXT NOT NULL DEFAULT '';
|
||||
`}, {"add-task-state", `
|
||||
ALTER TABLE tasks ADD COLUMN state TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN review_requests INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -758,7 +760,7 @@ func (s *Store) CreateTask(ctx context.Context, t api.Task, frozen *FrozenWorkfl
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
if parentTaskID != "" {
|
||||
open := false
|
||||
err := tx.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM tasks WHERE id = ? AND status = ? AND discard_state = '')`,
|
||||
err := tx.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM tasks WHERE id = ? AND status = ? AND discard_state = '' AND finalizing = '')`,
|
||||
parentTaskID, string(api.TaskStatusActive)).Scan(&open)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -811,7 +813,7 @@ func (s *Store) GetTask(ctx context.Context, id string) (api.Task, error) {
|
||||
func (s *Store) SetMergeTitle(ctx context.Context, id, title, actor string) error {
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
projectID := ""
|
||||
err := tx.QueryRowContext(ctx, `UPDATE tasks SET merge_title = ? WHERE id = ? AND status = ? AND merging_by = '' AND merge_queued_by = '' RETURNING project_id`,
|
||||
err := tx.QueryRowContext(ctx, `UPDATE tasks SET merge_title = ? WHERE id = ? AND status = ? AND merging_by = '' AND finalizing = '' AND merge_queued_by = '' RETURNING project_id`,
|
||||
title, id, string(api.TaskStatusActive)).Scan(&projectID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ErrNotFound
|
||||
@@ -826,7 +828,7 @@ func (s *Store) SetMergeTitle(ctx context.Context, id, title, actor string) erro
|
||||
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 = '' RETURNING project_id`, login, id).Scan(&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
|
||||
}
|
||||
@@ -869,6 +871,24 @@ func (s *Store) TakeMergingBy(ctx context.Context, id string) (string, error) {
|
||||
return login, err
|
||||
}
|
||||
|
||||
func (s *Store) ClearFinalizing(ctx context.Context, id string) error {
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
projectID := ""
|
||||
err := tx.QueryRowContext(ctx, `UPDATE tasks SET finalizing = '' WHERE id = ? AND finalizing != '' RETURNING project_id`, id).Scan(&projectID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return emit(api.Event{Kind: "task.merge_ended", ProjectId: &projectID, TaskId: &id, Actor: "byre", Payload: map[string]any{"id": id}})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) FinalizingTasks(ctx context.Context) ([]api.Task, error) {
|
||||
return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE status = ? AND finalizing != ''`, string(api.TaskStatusActive))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
@@ -921,9 +941,9 @@ func (s *Store) ActiveSubtasks(ctx context.Context, parentID string) ([]api.Task
|
||||
|
||||
func (s *Store) SetParentMerge(ctx context.Context, id, login string) error {
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
projectID, parentID, status, parentMergeBy, discardState := "", "", "", "", ""
|
||||
err := tx.QueryRowContext(ctx, `SELECT project_id, parent_task_id, status, parent_merge_by, discard_state FROM tasks WHERE id = ?`, id).
|
||||
Scan(&projectID, &parentID, &status, &parentMergeBy, &discardState)
|
||||
projectID, parentID, status, parentMergeBy, discardState, finalizing := "", "", "", "", "", ""
|
||||
err := tx.QueryRowContext(ctx, `SELECT project_id, parent_task_id, status, parent_merge_by, discard_state, finalizing FROM tasks WHERE id = ?`, id).
|
||||
Scan(&projectID, &parentID, &status, &parentMergeBy, &discardState, &finalizing)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ErrNotFound
|
||||
}
|
||||
@@ -935,6 +955,8 @@ func (s *Store) SetParentMerge(ctx context.Context, id, login string) error {
|
||||
return ErrNotFound
|
||||
case discardState != "":
|
||||
return ErrDiscardPending
|
||||
case finalizing != "":
|
||||
return ErrMerging
|
||||
case parentMergeBy != "":
|
||||
return ErrParentMergeRequested
|
||||
}
|
||||
@@ -1085,7 +1107,7 @@ func (s *Store) DiscardTask(ctx context.Context, id, actor string) (api.Task, er
|
||||
|
||||
func (s *Store) closeTask(ctx context.Context, id string, status api.TaskStatus, commit, actor, kind string, payload map[string]any) (api.Task, error) {
|
||||
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
res, err := tx.ExecContext(ctx, `UPDATE tasks SET status = ?, closed_at = ?, merge_commit = ? WHERE id = ? AND status = ?`,
|
||||
res, err := tx.ExecContext(ctx, `UPDATE tasks SET status = ?, closed_at = ?, merge_commit = ?, finalizing = '' WHERE id = ? AND status = ?`,
|
||||
string(status), now(), commit, id, string(api.TaskStatusActive))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1373,9 +1395,10 @@ func (s *Store) transitionSession(ctx context.Context, id string, from *api.Agen
|
||||
current := ""
|
||||
role := ""
|
||||
discard := ""
|
||||
finalizing := ""
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT coalesce(t.id, ''), coalesce(t.project_id, ''), s.owner, s.state, s.role, coalesce(t.discard_state, '') FROM agent_sessions s LEFT JOIN tasks t ON t.id = s.task_id WHERE s.id = ?
|
||||
`, id).Scan(&taskID, &projectID, &owner, ¤t, &role, &discard)
|
||||
SELECT coalesce(t.id, ''), coalesce(t.project_id, ''), s.owner, s.state, s.role, coalesce(t.discard_state, ''), coalesce(t.finalizing, '') FROM agent_sessions s LEFT JOIN tasks t ON t.id = s.task_id WHERE s.id = ?
|
||||
`, id).Scan(&taskID, &projectID, &owner, ¤t, &role, &discard, &finalizing)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ErrNotFound
|
||||
}
|
||||
@@ -1389,6 +1412,9 @@ func (s *Store) transitionSession(ctx context.Context, id string, from *api.Agen
|
||||
if state == api.AgentSessionStateRunning && role == string(api.Implementer) && discard != "" {
|
||||
return fmt.Errorf("%w: task %s is being discarded", ErrStateChanged, taskID)
|
||||
}
|
||||
if state == api.AgentSessionStateRunning && role == string(api.Implementer) && finalizing != "" {
|
||||
return fmt.Errorf("%w: task %s is finishing its merge", ErrStateChanged, taskID)
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, `UPDATE agent_sessions SET state = ? WHERE id = ?`, string(state), id)
|
||||
if err != nil {
|
||||
@@ -1586,7 +1612,7 @@ func settleTaskState(ctx context.Context, tx *sql.Tx, emit emitFunc, taskID, act
|
||||
const taskStateExpr = `CASE
|
||||
WHEN tasks.status != 'active' THEN tasks.status
|
||||
WHEN tasks.discard_state != '' THEN 'discarding'
|
||||
WHEN tasks.merging_by != '' THEN 'merging'
|
||||
WHEN tasks.merging_by != '' OR tasks.finalizing != '' THEN 'merging'
|
||||
ELSE coalesce((
|
||||
SELECT CASE
|
||||
WHEN s.state != 'idle' THEN s.state
|
||||
@@ -1643,7 +1669,7 @@ func (s *Store) ClearReviewRequests(ctx context.Context) error {
|
||||
|
||||
const projectColumns = `id, name, repo_path, default_branch, remote, created_by, created_at`
|
||||
|
||||
const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at, workflow_name, workflow_run, workflow_stage, parent_task_id, parent_merge_by, state, merging_by != '', discard_state != '' AND status = 'active', (SELECT coalesce(max(seq), 0) FROM events WHERE events.task_id = tasks.id AND kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1)`
|
||||
const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at, workflow_name, workflow_run, workflow_stage, parent_task_id, parent_merge_by, state, merging_by != '' OR finalizing != '', discard_state != '' AND status = 'active', (SELECT coalesce(max(seq), 0) FROM events WHERE events.task_id = tasks.id AND kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1)`
|
||||
|
||||
const sessionColumns = `id, coalesce(task_id, ''), owner, vendor, model, vendor_session_id, state, role, created_by, created_at, forked_from, forked_at`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user