Hold a task's merge while another task in the project is merging

This commit is contained in:
Ian Gulliver
2026-10-15 12:11:35 -07:00
parent ff22f33025
commit 606cf4b5c0
16 changed files with 546 additions and 190 deletions
+5 -1
View File
@@ -262,6 +262,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
tool("merge_task",
"Merge a task as the human: its agent merges the default branch in, then byre squashes it into one commit, pushes it to origin and closes the task. "+
"While another task in the project is merging, byre holds the merge and starts it when that one is done. "+
"Only when the human asks. You get a signal when it is merged; failures are filed in the task's inbox.",
taskOnly,
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
@@ -269,10 +270,13 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
if err != nil {
return "", err
}
_, err = m.Merge(ctx, task, project, user)
task, err = m.Merge(ctx, task, project, user)
if err != nil {
return "", err
}
if task.MergeQueuedAt != nil {
return fmt.Sprintf("Merge of %s held until the merge in progress in its project is done.", task.Id), nil
}
return fmt.Sprintf("Merge of %s started.", task.Id), nil
}),
+18 -3
View File
@@ -66,6 +66,9 @@ type Manager struct {
setups map[string]bool
setupFailures map[string]string
mergeMu sync.Mutex
mergeHolders map[string]string
bad *badLines
watchStop chan struct{}
}
@@ -111,6 +114,7 @@ func New(st *store.Store, limits Limits, worktreeRoot, leadRoot string) *Manager
snaps: map[string]*snapState{},
setups: map[string]bool{},
setupFailures: map[string]string{},
mergeHolders: map[string]string{},
bad: newBadLines(),
watchStop: make(chan struct{}),
}
@@ -270,6 +274,11 @@ func (m *Manager) retire(ctx context.Context, sess api.AgentSession, actor strin
}
func (m *Manager) Recover(ctx context.Context) error {
err := m.recoverMerges(ctx)
if err != nil {
return err
}
sessions, err := m.store.SessionsInState(ctx, api.AgentSessionStateRunning)
if err != nil {
return err
@@ -290,7 +299,7 @@ func (m *Manager) Recover(ctx context.Context) error {
return err
}
if sess.Owner == "" {
m.takeMerge(task.Id)
m.abandonMerge(task.Id, task.ProjectId)
}
continue
}
@@ -312,6 +321,7 @@ func (m *Manager) Recover(ctx context.Context) error {
}
go m.deliverAllLeads()
go m.startHeldMerges(context.Background())
return m.drain(ctx)
}
@@ -764,7 +774,7 @@ func (m *Manager) pump(l *live) {
slog.Error("record session exit", "session", l.id, "err", err)
}
if l.owner == "" {
m.takeMerge(l.taskID)
m.abandonMerge(l.taskID, l.projectID)
m.fileError(l, "The agent process exited mid-turn. Send a message to resume it.")
}
@@ -925,10 +935,15 @@ func (m *Manager) handle(ctx context.Context, l *live, ev claude.Event) error {
return nil
}
mergeBy := m.takeMerge(l.taskID)
if mergeBy != "" && finished {
switch {
case mergeBy != "" && finished:
m.logUndelivered(ctx, l.id, "turn ended in merge")
go m.finishMerge(l, mergeBy)
return nil
case mergeBy != "":
go m.releaseMerge(l.projectID, l.taskID)
default:
go m.nextMerge(l.projectID)
}
err = m.Deliver(ctx, l.id)
if err != nil {
+69
View File
@@ -917,6 +917,75 @@ func TestMergeFetchesMovedRemote(t *testing.T) {
}
}
func TestMergeHeldWhileAnotherMerges(t *testing.T) {
t.Parallel()
e := newEnv(t)
m := e.manager()
second := e.addTask()
for i, task := range []api.Task{e.task, second} {
_, err := m.Create(e.ctx, task, e.user, fmt.Sprintf("Create a file task%d.txt containing exactly the word task.", i), testModel)
if err != nil {
t.Fatalf("create session: %v", err)
}
e.waitIdle()
}
first, err := m.Merge(e.ctx, e.task, e.project, e.user)
if err != nil {
t.Fatalf("merge first: %v", err)
}
if first.MergeQueuedAt != nil {
t.Fatalf("first merge held: %+v", first)
}
held, err := m.Merge(e.ctx, second, e.project, e.user)
if err != nil {
t.Fatalf("merge second: %v", err)
}
if held.MergeQueuedAt == nil {
t.Fatalf("second merge not held: %+v", held)
}
_, err = m.Merge(e.ctx, held, e.project, e.user)
if !errors.Is(err, agents.ErrConflict) {
t.Fatalf("merging a held task again: %v, want conflict", err)
}
events := e.waitFor("second task merged", func(ev api.Event) bool {
return ev.Kind == "task.merged" && *ev.TaskId == second.Id
})
firstMerged, unqueued := -1, -1
for i, ev := range events {
switch {
case ev.Kind == "task.merged" && *ev.TaskId == e.task.Id:
firstMerged = i
case ev.Kind == "task.merge_unqueued" && *ev.TaskId == second.Id:
unqueued = i
case ev.Kind == "inbox.created" && ev.Payload["kind"] == string(api.SessionError):
t.Fatalf("merge filed an error: %v", ev.Payload)
}
}
if firstMerged < 0 || unqueued < firstMerged {
t.Fatalf("held merge started at event %d, first merged at %d", unqueued, firstMerged)
}
tasks := map[string]api.Task{}
for _, id := range []string{e.task.Id, second.Id} {
tasks[id], err = e.store.GetTask(e.ctx, id)
if err != nil {
t.Fatalf("get task: %v", err)
}
if tasks[id].Status != api.TaskStatusMerged || tasks[id].MergeCommit == nil || tasks[id].MergeQueuedAt != nil {
t.Fatalf("task after merge %+v", tasks[id])
}
}
if head := gitOut(t, e.remote, "rev-parse", "main"); head != *tasks[second.Id].MergeCommit {
t.Fatalf("remote main at %s, want second merge commit %s", head, *tasks[second.Id].MergeCommit)
}
if parent := gitOut(t, e.remote, "rev-parse", "main^"); parent != *tasks[e.task.Id].MergeCommit {
t.Fatalf("second merge commit parent %s, want first merge commit %s", parent, *tasks[e.task.Id].MergeCommit)
}
}
func TestCreateTaskTool(t *testing.T) {
t.Parallel()
e := newEnv(t)
+158 -7
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"log/slog"
"slices"
"strings"
"unicode/utf8"
@@ -40,26 +41,169 @@ func (m *Manager) MergeIn(ctx context.Context, task api.Task, user api.User, tar
return m.create(ctx, task, user, prompt, "", "byre", nil)
}
func (m *Manager) Merge(ctx context.Context, task api.Task, project api.Project, user api.User) (api.AgentSession, error) {
func (m *Manager) Merge(ctx context.Context, task api.Task, project api.Project, user api.User) (api.Task, error) {
if user.GitName == "" || user.GitEmail == "" {
return api.AgentSession{}, ErrNoGitIdentity
return api.Task{}, ErrNoGitIdentity
}
if task.MergeQueuedAt != nil {
return api.Task{}, fmt.Errorf("%w: task %s is already waiting to merge", ErrConflict, task.Id)
}
m.mergeMu.Lock()
defer m.mergeMu.Unlock()
switch m.mergeHolders[project.Id] {
case task.Id:
return api.Task{}, fmt.Errorf("%w: task %s is already merging", ErrConflict, task.Id)
case "":
err := m.startMerge(ctx, task, project, user)
if err != nil {
return api.Task{}, err
}
default:
err := m.store.QueueMerge(ctx, task.Id, user.Login)
if errors.Is(err, store.ErrNotFound) {
return api.Task{}, fmt.Errorf("%w: task %s is closed or already waiting to merge", ErrConflict, task.Id)
}
if err != nil {
return api.Task{}, err
}
}
return m.store.GetTask(ctx, task.Id)
}
func (m *Manager) startMerge(ctx context.Context, task api.Task, project api.Project, user api.User) error {
err := git.FetchFastForward(project.RepoPath, project.DefaultBranch)
if err != nil {
return api.AgentSession{}, err
return err
}
err = m.store.SetMergingBy(ctx, task.Id, user.Login)
if err != nil {
return api.AgentSession{}, err
return err
}
m.mergeHolders[project.Id] = task.Id
sess, err := m.MergeIn(ctx, task, user, project.DefaultBranch)
_, err = m.MergeIn(ctx, task, user, project.DefaultBranch)
if err != nil {
m.takeMerge(task.Id)
delete(m.mergeHolders, project.Id)
}
return sess, err
return err
}
func (m *Manager) recoverMerges(ctx context.Context) error {
merging, err := m.store.MergingTasks(ctx)
if err != nil {
return err
}
m.mergeMu.Lock()
defer m.mergeMu.Unlock()
for _, t := range merging {
m.mergeHolders[t.ProjectId] = t.Id
}
return nil
}
func (m *Manager) startHeldMerges(ctx context.Context) {
projects, err := m.store.ProjectsWithQueuedMerges(ctx)
if err != nil {
slog.Error("list projects with held merges", "err", err)
return
}
for _, p := range projects {
m.nextMerge(p)
}
}
func (m *Manager) abandonMerge(taskID, projectID string) {
if m.takeMerge(taskID) == "" {
return
}
go m.releaseMerge(projectID, taskID)
}
func (m *Manager) releaseMerge(projectID, taskID string) {
m.mergeMu.Lock()
defer m.mergeMu.Unlock()
if m.mergeHolders[projectID] == taskID {
delete(m.mergeHolders, projectID)
}
m.startNextMerge(projectID)
}
func (m *Manager) nextMerge(projectID string) {
m.mergeMu.Lock()
defer m.mergeMu.Unlock()
m.startNextMerge(projectID)
}
func (m *Manager) startNextMerge(projectID string) {
if m.mergeHolders[projectID] != "" {
return
}
ctx := context.Background()
held, err := m.store.QueuedMerges(ctx, projectID)
if err != nil {
slog.Error("list held merges", "project", projectID, "err", err)
return
}
for _, task := range held {
sessions, err := m.store.ListSessions(ctx, task.Id)
if err != nil {
slog.Error("list sessions for held merge", "task", task.Id, "err", err)
continue
}
if slices.ContainsFunc(sessions, func(s api.AgentSession) bool {
return s.State == api.AgentSessionStateRunning || s.State == api.AgentSessionStateQueued
}) {
continue
}
login, err := m.store.TakeQueuedMerge(ctx, task.Id)
if err != nil {
slog.Error("take held merge", "task", task.Id, "err", err)
continue
}
if login == "" {
continue
}
err = m.startHeldMerge(ctx, task, login)
if err == nil {
slog.Info("started held merge", "task", task.Id, "by", login)
return
}
slog.Error("start held merge", "task", task.Id, "err", err)
m.fileTaskError(ctx, task, sessions, "Starting the held merge failed: "+err.Error())
}
}
func (m *Manager) startHeldMerge(ctx context.Context, task api.Task, login string) error {
project, err := m.store.GetProject(ctx, task.ProjectId)
if err != nil {
return err
}
user, err := m.store.GetUser(ctx, login)
if err != nil {
return err
}
if user.GitName == "" || user.GitEmail == "" {
return ErrNoGitIdentity
}
return m.startMerge(ctx, task, project, user)
}
func (m *Manager) fileTaskError(ctx context.Context, task api.Task, sessions []api.AgentSession, msg string) {
if len(sessions) == 0 {
slog.Error("no session to file task error on", "task", task.Id, "error", msg)
return
}
last := sessions[len(sessions)-1]
m.fileError(&live{id: last.Id, taskID: task.Id, projectID: task.ProjectId}, msg)
}
func (m *Manager) takeMerge(taskID string) string {
@@ -71,6 +215,8 @@ func (m *Manager) takeMerge(taskID string) string {
}
func (m *Manager) finishMerge(l *live, login string) {
defer m.releaseMerge(l.projectID, l.taskID)
ctx := context.Background()
err := m.squashAndClose(ctx, l.taskID, login)
if err == nil {
@@ -170,7 +316,12 @@ func (m *Manager) Discard(ctx context.Context, task api.Task, project api.Projec
if err != nil {
return api.Task{}, err
}
return m.store.DiscardTask(ctx, task.Id, actor)
t, err := m.store.DiscardTask(ctx, task.Id, actor)
if err != nil {
return api.Task{}, err
}
go m.releaseMerge(project.Id, task.Id)
return t, nil
}
func (m *Manager) RemoveTask(ctx context.Context, task api.Task, project api.Project, actor string) error {
+153 -148
View File
@@ -812,6 +812,9 @@ type Task struct {
// MergeCommit merged: the squash commit on the default branch; empty when there was nothing to merge.
MergeCommit *string `json:"merge_commit,omitempty"`
// MergeQueuedAt When Merge was requested while another task in the project was merging; absent once the merge starts or when none is held.
MergeQueuedAt *time.Time `json:"merge_queued_at,omitempty"`
// MergeTitle The title of the squash commit Merge makes: the task title until the agent sets another with set_merge_title.
MergeTitle string `json:"merge_title"`
@@ -2683,154 +2686,156 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
// const string: with thousands of chunks the chained `+` fold is several
// times slower for the Go compiler than parsing a slice literal.
var swaggerSpec = []string{
"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",
"p93hDx/+fwAAAP//",
"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",
"psIcQ62m/PNwc870d7vDHz/+3wAAAP//",
}
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
+2 -2
View File
@@ -387,12 +387,12 @@ func (s *Server) MergeTask(w http.ResponseWriter, r *http.Request, taskID string
return
}
sess, err := s.agents.Merge(r.Context(), t, p, u)
t, err = s.agents.Merge(r.Context(), t, p, u)
if err != nil {
writeAgentError(w, err)
return
}
writeJSON(w, http.StatusAccepted, sess)
writeJSON(w, http.StatusAccepted, t)
}
func (s *Server) MergeIn(w http.ResponseWriter, r *http.Request, taskID string) {
+98 -17
View File
@@ -268,6 +268,9 @@ UPDATE tasks SET port_base = (
SELECT 41000 + 10 * (count(*) - 1) FROM tasks AS earlier
WHERE earlier.status = 'active' AND (earlier.created_at < tasks.created_at OR (earlier.created_at = tasks.created_at AND earlier.id <= tasks.id))
) WHERE status = 'active' AND port_base = 0;
`}, {"add-task-merge-queue", `
ALTER TABLE tasks ADD COLUMN merge_queued_by TEXT NOT NULL DEFAULT '';
ALTER TABLE tasks ADD COLUMN merge_queued_at TEXT NOT NULL DEFAULT '';
`}}
const (
@@ -570,21 +573,7 @@ func (s *Store) CreateTask(ctx context.Context, t api.Task, actor string) error
}
func (s *Store) ListTasks(ctx context.Context, projectID string) ([]api.Task, error) {
rows, err := s.db.QueryContext(ctx, `SELECT `+taskColumns+` FROM tasks WHERE project_id = ? ORDER BY created_at DESC`, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
tasks := []api.Task{}
for rows.Next() {
t, err := scanTask(rows)
if err != nil {
return nil, err
}
tasks = append(tasks, t)
}
return tasks, rows.Err()
return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE project_id = ? ORDER BY created_at DESC`, projectID)
}
func (s *Store) GetTask(ctx context.Context, id string) (api.Task, error) {
@@ -633,6 +622,90 @@ func (s *Store) TakeMergingBy(ctx context.Context, id string) (string, error) {
return login, 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 {
return nil, err
}
defer rows.Close()
tasks := []api.Task{}
for rows.Next() {
t, err := scanTask(rows)
if err != nil {
return nil, err
}
tasks = append(tasks, t)
}
return tasks, rows.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 = '' 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 {
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
}
return emit(api.Event{Kind: "task.merge_unqueued", ProjectId: &projectID, TaskId: &id, Actor: "byre", Payload: map[string]any{"id": id}})
})
return login, err
}
func (s *Store) MergeTask(ctx context.Context, id, commit, pushed, actor string) (api.Task, error) {
return s.closeTask(ctx, id, api.TaskStatusMerged, commit, actor, "task.merged", map[string]any{"id": id, "commit": commit, "pushed": pushed})
}
@@ -1007,7 +1080,7 @@ func (s *Store) write(ctx context.Context, fn func(tx *sql.Tx, emit emitFunc) er
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, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at`
const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at`
const sessionColumns = `id, coalesce(task_id, ''), owner, vendor, model, vendor_session_id, state, role, created_by, created_at, forked_from, forked_at`
@@ -1035,7 +1108,8 @@ func scanTask(row scanner) (api.Task, error) {
todoItem := ""
forkedFrom := ""
forkedAt := 0
err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt)
mergeQueuedAt := ""
err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt)
if err != nil {
return t, err
}
@@ -1064,6 +1138,13 @@ func scanTask(row scanner) (api.Task, error) {
t.ForkedFrom = &forkedFrom
t.ForkedAt = &forkedAt
}
if mergeQueuedAt != "" {
qt, err := parseTime(mergeQueuedAt)
if err != nil {
return t, err
}
t.MergeQueuedAt = &qt
}
return t, nil
}