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
+7 -4
View File
@@ -310,14 +310,16 @@ paths:
Starts the same agent turn as merge-in. When that turn ends normally, byre squashes the task's worktree
into one commit titled with the task's merge_title on the project's default branch, authored by the caller with
a Co-authored-by trailer for each of the task's co-authors, pushes it to origin, fast-forwards the project's
checkout to it, then removes the worktree and branch and marks the task merged. Refused while a turn is running
or queued.
checkout to it, then removes the worktree and branch and marks the task merged. While another task in the
project is merging, the merge is held instead (merge_queued_at is set) and starts when no merge is in progress
in the project and the task's session is idle, oldest first. Refused while a turn is running or queued, or the
merge is already held.
responses:
"202":
description: The merge turn was started or queued.
description: The merge was started or held.
content:
application/json:
schema: { $ref: "#/components/schemas/AgentSession" }
schema: { $ref: "#/components/schemas/Task" }
"400": { $ref: "#/components/responses/Error" }
"404": { $ref: "#/components/responses/Error" }
"409": { $ref: "#/components/responses/Error" }
@@ -864,6 +866,7 @@ components:
todo_item: { type: string, description: "The title of the TODO.md item the task was started from; absent for other tasks." }
forked_from: { type: string, description: "The task this one was forked from; absent for other tasks." }
forked_at: { type: integer, description: "With forked_from: the n of the snapshot it was forked at." }
merge_queued_at: { type: string, format: date-time, description: "When Merge was requested while another task in the project was merging; absent once the merge starts or when none is held." }
Todo:
type: object
+2
View File
@@ -41,6 +41,8 @@ A task's participants are the users who put a message bubble with their name in
Before starting the turn, Merge runs `git fetch origin <default branch>` and `git merge --ff-only origin/<default branch>` in the checkout, so the agent merges in what the remote has; the request waits for the fetch, and a failure (network, or a checkout with commits the remote lacks) is returned as the request's error. If the push is still refused, for example because someone pushed during the turn, or fails for any other reason, byre files the error in the inbox and the task stays active; clicking Merge again fetches and goes round once more. A refused push leaves the checkout's default branch where it was, since the squash commit was never moved onto it. If the squash fails, for example because the default branch moved again during the turn, byre also files an error and the task stays active. An interrupted or failed turn, an agent process that exits mid-turn, or a session that can't be resumed after a restart clears `merging_by` and leaves the task active without squashing. `POST /api/tasks/{id}/discard` removes everything without merging.
One merge runs per project at a time. The Manager keeps the task holding each project's merge in memory (`mergeHolders` in `internal/agents/manager.go`), from just before the fetch until the merge closes the task, fails, or is abandoned as above, or the task is discarded; on startup it is rebuilt from the active tasks with `merging_by` set. Merge on another task of that project while one is held doesn't fetch or start a turn: it records the request in `tasks.merge_queued_by` and `merge_queued_at` (`merge_queued_at` on the Task), emits `task.merge_queued` (a transcript note), and returns the task with 202. A second Merge on a held or merging task is a 409. Whenever a project's merge is released, any task turn in it ends, and on startup, byre starts the oldest held merge in the project whose task session is not `running` or `queued`: it clears the request (emitting `task.merge_unqueued`, not drawn) and goes through the same fetch and turn as a direct Merge by the user who asked, so it merges in what the earlier merge pushed. If that start fails (the fetch, or the user has no git identity), byre files the error in the task's inbox and tries the next held merge. The Merge button reads **Merge held**, disabled, while the task has `merge_queued_at`; discarding the task drops the request. `sqlite3 byre.db "SELECT id, merge_queued_by, merge_queued_at FROM tasks WHERE merge_queued_by != ''"` lists held merges.
## Running as a service
The listen address is `listenAddr` in `cmd/byre/serve.go`; it binds loopback only.
+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
}
+2
View File
@@ -229,6 +229,8 @@ export function handleEvent(ev) {
break;
case "task.created":
case "task.merge_title":
case "task.merge_queued":
case "task.merge_unqueued":
case "task.merged":
case "task.discarded":
if (ev.project_id) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+9 -1
View File
@@ -75,7 +75,13 @@ function taskHead(rerender) {
}
const target = state.projects.find((p) => p.id === task.project_id)?.default_branch ?? "the default branch";
const busy = sess?.state === "running" || sess?.state === "queued";
const merge = h("button", { disabled: busy || unmerged?.taskId !== task.id || !unmerged.has, title: `The agent merges ${target} in, then byre squashes into one commit on ${target} and pushes it to origin` }, "Merge");
const held = task.merge_queued_at !== undefined;
const merge = h("button", {
disabled: busy || held || unmerged?.taskId !== task.id || !unmerged.has,
title: held
? "Waiting for the merge in progress in this project to finish"
: `The agent merges ${target} in, then byre squashes into one commit on ${target} and pushes it to origin; held while another task in the project is merging`,
}, held ? "Merge held" : "Merge");
merge.onclick = async () => {
let coAuthors;
try {
@@ -848,6 +854,8 @@ function renderEvent(ev, ctx) {
}
case "task.merge_title":
return h("div", { class: "note" }, `${ev.actor} set the merge commit title to "${String(p["merge_title"])}" `, when);
case "task.merge_queued":
return h("div", { class: "note" }, `${ev.actor} asked to merge; held until the merge in progress in this project is done `, when);
case "setup.started": {
const base = Number(p["port_base"]);
return h("div", { class: "note" }, `running ${String(p["script"])} with ports ${base}–${base + 9} `, when);
+1 -1
View File
File diff suppressed because one or more lines are too long
+8 -3
View File
@@ -296,7 +296,7 @@ export interface paths {
};
get?: never;
put?: never;
/** @description Starts the same agent turn as merge-in. When that turn ends normally, byre squashes the task's worktree into one commit titled with the task's merge_title on the project's default branch, authored by the caller with a Co-authored-by trailer for each of the task's co-authors, pushes it to origin, fast-forwards the project's checkout to it, then removes the worktree and branch and marks the task merged. Refused while a turn is running or queued. */
/** @description Starts the same agent turn as merge-in. When that turn ends normally, byre squashes the task's worktree into one commit titled with the task's merge_title on the project's default branch, authored by the caller with a Co-authored-by trailer for each of the task's co-authors, pushes it to origin, fast-forwards the project's checkout to it, then removes the worktree and branch and marks the task merged. While another task in the project is merging, the merge is held instead (merge_queued_at is set) and starts when no merge is in progress in the project and the task's session is idle, oldest first. Refused while a turn is running or queued, or the merge is already held. */
post: operations["mergeTask"];
delete?: never;
options?: never;
@@ -813,6 +813,11 @@ export interface components {
forked_from?: string;
/** @description With forked_from: the n of the snapshot it was forked at. */
forked_at?: number;
/**
* Format: date-time
* @description When Merge was requested while another task in the project was merging; absent once the merge starts or when none is held.
*/
merge_queued_at?: string;
};
Todo: {
/** @description Whether the default branch has a TODO.md at its root. */
@@ -1681,13 +1686,13 @@ export interface operations {
};
requestBody?: never;
responses: {
/** @description The merge turn was started or queued. */
/** @description The merge was started or held. */
202: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AgentSession"];
"application/json": components["schemas"]["Task"];
};
};
400: components["responses"]["Error"];
+2
View File
@@ -285,6 +285,8 @@ export function handleEvent(ev: ByreEvent): void {
break;
case "task.created":
case "task.merge_title":
case "task.merge_queued":
case "task.merge_unqueued":
case "task.merged":
case "task.discarded":
if (ev.project_id) {
+11 -2
View File
@@ -134,10 +134,16 @@ function taskHead(rerender: () => void): Head {
}
const target = state.projects.find((p) => p.id === task.project_id)?.default_branch ?? "the default branch";
const busy = sess?.state === "running" || sess?.state === "queued";
const held = task.merge_queued_at !== undefined;
const merge = h(
"button",
{ disabled: busy || unmerged?.taskId !== task.id || !unmerged.has, title: `The agent merges ${target} in, then byre squashes into one commit on ${target} and pushes it to origin` },
"Merge",
{
disabled: busy || held || unmerged?.taskId !== task.id || !unmerged.has,
title: held
? "Waiting for the merge in progress in this project to finish"
: `The agent merges ${target} in, then byre squashes into one commit on ${target} and pushes it to origin; held while another task in the project is merging`,
},
held ? "Merge held" : "Merge",
);
merge.onclick = async () => {
let coAuthors;
@@ -1008,6 +1014,9 @@ function renderEvent(ev: ByreEvent, ctx: Ctx): HTMLElement | null {
case "task.merge_title":
return h("div", { class: "note" }, `${ev.actor} set the merge commit title to "${String(p["merge_title"])}" `, when);
case "task.merge_queued":
return h("div", { class: "note" }, `${ev.actor} asked to merge; held until the merge in progress in this project is done `, when);
case "setup.started": {
const base = Number(p["port_base"]);
return h("div", { class: "note" }, `running ${String(p["script"])} with ports ${base}–${base + 9} `, when);