diff --git a/TODO.md b/TODO.md index 2f9d6dc..ee755f1 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,5 @@ # Bugs -- Task accepts new turns while its merge is being finalized - `EndTurn` clears `merging_by` (`internal/agents/manager.go:899`, `:926–930`) before `finishMerge` snapshots the live worktree, pushes and runs `RemoveTask` (`internal/agents/mergein.go:238–318`), and `transitionSession` blocks only discard (`internal/store/store.go:1389`). A message sent during the push can put half-done edits into the pushed squash, or be killed when the worktree is force-removed. Add a persisted `finalizing` marker set in the same EndTurn transaction; turns, discard and subtask creation refuse while it is set, and the UI shows it as Merging. - Merge finalization keeps no durable progress; cleanup runs before the task is closed `internal/agents/mergein.go:303–324` runs push → notes → fast-forward → `RemoveTask` → `MergeTask`. A crash or error after the push leaves an active task with no worktree; a retried Merge fails in `Snapshot`, or squashes to no change and closes with an empty `merge_commit`. Have the `finalizing` marker record the squash commit; on startup, a recorded commit that is an ancestor of origin/ finishes notes, fast-forward, removal and MergeTask idempotently, otherwise the marker is cleared and an error is filed. - A failed auto-commit still advances the workflow from the old HEAD diff --git a/api/openapi.yaml b/api/openapi.yaml index 995e020..224afbb 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1043,7 +1043,7 @@ components: description: "What the task is doing, kept by the store in the same transaction as every change to it (a task.state event carries each change): merged or discarded once closed, else discarding, merging, the state of its agent's latest live session when that isn't idle, reviewing while a review runs, review_pending between a turn's end and its snapshot's review starting or being skipped, idle, or no_agent." enum: [running, idle, review_pending, reviewing, merging, discarding, merged, discarded, no_agent] x-enum-varnames: [TaskStateRunning, TaskStateIdle, TaskStateReviewPending, TaskStateReviewing, TaskStateMerging, TaskStateDiscarding, TaskStateMerged, TaskStateDiscarded, TaskStateNoAgent] - merging: { type: boolean, description: "Whether a Merge was started and its turn hasn't ended yet." } + merging: { type: boolean, description: "Whether a Merge was started and hasn't finished: its turn is running, or the turn ended and byre is squashing, pushing and closing the task." } discarding: { type: boolean, description: "Whether a Discard was started and the task isn't closed yet: its summary turn is waiting for the running turn, running, or done and the task is being removed." } parent_task_id: { type: string, description: "The task whose agent created this one as its subtask; absent for other tasks." } merging_into_parent: { type: boolean, description: "Whether a merge of this subtask into its parent was requested and the parent hasn't merged it yet." } diff --git a/docs/operations.md b/docs/operations.md index 1c9ea5e..6b022d5 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -53,7 +53,7 @@ Agents find closed tasks with the `search_closed_tasks` MCP tool, which task age `sqlite3 byre.db "SELECT id, discard_state, discarding_by, discard_session FROM tasks WHERE discard_state != '' AND status = 'active'"` lists discards in progress. Confirming Discard in the UI points the right pane away from the task at once, while its summary turn runs. -A Task's `merging` is true while `merging_by` is set, and `discarding` while the task is active with `discard_state` set. Clearing `merging_by` emits `task.merge_ended` (not drawn); that, `task.merge_started`, `task.discard_started` and `task.discarded` reload the project's tasks in the UI, so the task switcher and the Project tab show the task as **Merging**, or **Working** while discarding (see "Task state" under the task switcher). +A Task's `merging` is true while `merging_by` or `finalizing` is set, and `discarding` while the task is active with `discard_state` set. Clearing `merging_by` without moving it to `finalizing`, or clearing `finalizing` without closing the task, emits `task.merge_ended` (not drawn); that, `task.merge_started`, `task.discard_started` and `task.discarded` reload the project's tasks in the UI, so the task switcher and the Project tab show the task as **Merging**, or **Working** while discarding (see "Task state" under the task switcher). ### Task state in the store @@ -65,11 +65,11 @@ The UI's labels (see "Task state" under the task switcher) read `state` for **Wo `POST /api/tasks/{id}/merge-in` (the transcript's **Sync from main** button, labelled with the default branch) brings the default branch into the task branch through the agent: it sends `mergeInPrompt` (`internal/agents/mergein.go`) as a `byre` message to the task's session, or starts a session with it when the task has none. The prompt has the agent run `git merge --no-ff --no-commit`, resolve conflicts, regenerate generated files rather than hand-merging them, check the build and tests, and leave the merge uncommitted; byre's end-of-turn commit concludes it as a merge commit. If resolving the conflicts took substantive changes rather than combining both sides as written, the prompt tells the agent to stop there and end its turn describing them; the turn's snapshot then gets its review like any other (see "Reviews"). byre doesn't inspect the merge itself: whether a change is substantive, and whether to stop, is the agent's call. On a Merge turn the prompt adds that byre squashes and pushes when the turn ends, and tells the agent to call the `stop_merge` MCP tool first if it stops (see below). The prompt ends by showing the task's current merge title and asking the agent to set a better one with `set_merge_title` if it no longer describes all the work on the branch. It is refused (409) while a session is `running`. -A task's `merge_title` is the title of the squash commit Merge makes. It is the task title until the agent calls the `set_merge_title` MCP tool (`internal/agents/mergein.go`), which it may do until a merge is confirmed: one line of at most 72 characters (`maxMergeTitle`, checked by `cleanMergeTitle`), stored in `tasks.merge_title` (empty there means "use the title"), recorded as a `task.merge_title` event that the transcript shows as a note. Malformed titles are rejected with a corrective message, and so is a call once the task is closed or while `merging_by` or `merge_queued_by` is set. The Merge confirm dialog shows the title in a text field the human can edit (Enter confirms), lists the co-authors, and sends the field's value as `merge_title` in the body of `POST /api/tasks/{id}/merge`. Merge validates it like the tool does (400 when malformed) and, when it differs from the stored title, stores it with the human as the event's actor before starting or holding the merge. +A task's `merge_title` is the title of the squash commit Merge makes. It is the task title until the agent calls the `set_merge_title` MCP tool (`internal/agents/mergein.go`), which it may do until a merge is confirmed: one line of at most 72 characters (`maxMergeTitle`, checked by `cleanMergeTitle`), stored in `tasks.merge_title` (empty there means "use the title"), recorded as a `task.merge_title` event that the transcript shows as a note. Malformed titles are rejected with a corrective message, and so is a call once the task is closed or while `merging_by`, `finalizing` or `merge_queued_by` is set. The Merge confirm dialog shows the title in a text field the human can edit (Enter confirms), lists the co-authors, and sends the field's value as `merge_title` in the body of `POST /api/tasks/{id}/merge`. Merge validates it like the tool does (400 when malformed) and, when it differs from the stored title, stores it with the human as the event's actor before starting or holding the merge. A task's participants are the users who put a message bubble with their name in its transcript, derived from its events (`Store.Participants`): the actors of its `user.message` events, joined to `users` so `byre` and `agent` drop out. That covers messages sent to the agent and inbox answers, whose delivery message carries the name of the first responder in it (`Deliver` in `internal/agents/inbox.go`); dismissing an item puts nothing in the transcript and doesn't count. A fork's participants also include the user who forked it, the actor of its `task.created` event (whose payload carries `forked_from`), whose "forked this task" note opens its transcript. They are ordered by their first such event. `GET /api/tasks/{id}/co-authors` returns the participants other than the caller, with their git name and email from `PUT /api/me` (empty for a user who hasn't set one). The Merge confirm fetches it and lists `Name ` for each co-author, and separately those with no git identity, who get no trailer. -`POST /api/tasks/{id}/merge` starts the same turn, with the prompt's closing invitation to revise the title left out since the human just confirmed it and a request to call `set_task_summary` in its place, and records in `tasks.merging_by` that the task is to be merged by the requesting user (emitting `task.merge_started` with that user as actor, not drawn), who must have a git identity (400 otherwise). Every turn end of the task's session reads and clears it. It survives a byre restart during the turn: startup resumes the session with the restart notice, and the merge happens when that resumed turn ends. When that turn ends normally (not interrupted, not an error), byre squashes the worktree, now containing the default branch, onto it with `git merge-tree --merge-base`, makes one commit titled with the task's `merge_title` with `git commit-tree`, authored and committed by the merging user with no agent attribution, its body one `Co-authored-by: Name ` trailer per co-author with a git identity (`squashMessage` in `internal/agents/mergein.go`; the co-authors are computed when the squash runs, relative to the user who clicked Merge), and pushes that commit to the default branch on the `origin` remote of the project's checkout together with its note (below), in one atomic push (`git push --atomic --porcelain origin :refs/heads/ :refs/notes/byre`, run in the checkout), so either both land or neither does. Only once the push is accepted does it point the checkout's `refs/notes/byre` at the notes commit, run `git merge --ff-only` in the project's checkout (which must have the default branch checked out) and close the task; the `task.merged` event carries the commit and `pushed` (`origin/`), and the transcript shows both. When the merge changes nothing, no commit is made and nothing is pushed. +`POST /api/tasks/{id}/merge` starts the same turn, with the prompt's closing invitation to revise the title left out since the human just confirmed it and a request to call `set_task_summary` in its place, and records in `tasks.merging_by` that the task is to be merged by the requesting user (emitting `task.merge_started` with that user as actor, not drawn), who must have a git identity (400 otherwise). Every turn end of the task's session reads and clears it. It survives a byre restart during the turn: startup resumes the session with the restart notice, and the merge happens when that resumed turn ends. When that turn ends normally (not interrupted, not an error), the transaction that ends it (`Store.EndTurn`) moves the login from `merging_by` to `tasks.finalizing`, so the task stays **Merging** while byre squashes, pushes and closes it. While `finalizing` is set, the task's implementer session can't start a turn (a message or Sync is a 409; the store refuses the `running` transition in `transitionSession`), Discard and merging into a parent are refused as merging, `set_merge_title` is refused, and the task can't get new subtasks (`ErrParentNotOpen`). Closing the task clears it; a failed squash or push clears it (emitting `task.merge_ended`) before filing the error, so the queued inbox responses can be delivered. A byre restart while it is set clears it at startup (`recoverFinalizing` in `internal/agents/mergein.go`) and files an error on the task saying the merge may not have been pushed and the worktree may be gone, since nothing records how far it got. `sqlite3 byre.db "SELECT id, finalizing FROM tasks WHERE finalizing != ''"` lists merges being finished. Then byre squashes the worktree, now containing the default branch, onto it with `git merge-tree --merge-base`, makes one commit titled with the task's `merge_title` with `git commit-tree`, authored and committed by the merging user with no agent attribution, its body one `Co-authored-by: Name ` trailer per co-author with a git identity (`squashMessage` in `internal/agents/mergein.go`; the co-authors are computed when the squash runs, relative to the user who clicked Merge), and pushes that commit to the default branch on the `origin` remote of the project's checkout together with its note (below), in one atomic push (`git push --atomic --porcelain origin :refs/heads/ :refs/notes/byre`, run in the checkout), so either both land or neither does. Only once the push is accepted does it point the checkout's `refs/notes/byre` at the notes commit, run `git merge --ff-only` in the project's checkout (which must have the default branch checked out) and close the task; the `task.merged` event carries the commit and `pushed` (`origin/`), and the transcript shows both. When the merge changes nothing, no commit is made and nothing is pushed. Before starting the turn, Merge runs `git fetch origin` (every configured refspec, so the notes too) and `git merge --ff-only origin/` 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. The agent calls `stop_merge` (`internal/agents/mergein.go`) with a `reason` when its conflict resolution took substantive changes: it clears `merging_by` and releases the project's merge at once, and records a `task.merge_stopped` event carrying the reason, which the transcript shows as a note. The turn then ends without a squash, the task stays active, its snapshot is reviewed, and the human clicks Merge again when satisfied. Called with no merge pending (a Sync turn, say), it changes nothing and tells the agent so. 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; a turn that ends interrupted or with an empty final message also files an error in the inbox saying the merge didn't happen. A Claude `result` with `num_turns` 0, no `result` text and no error, outside an interrupt, isn't a turn end: the CLI emits one on its own, for example after a resume when it reports a background shell left over from the previous process, possibly after byre has already sent the next message. byre logs `ignored result with no model turns`, records it as an `agent.vendor` event, and keeps the turn running, so it can't take `merging_by` from the Merge turn that follows. Merge (either target, see "Subtasks") and Discard are refused (409, naming them) while the task has active subtasks, whoever asks: the button, the lead or an agent. @@ -196,7 +196,7 @@ Each user has one long-running lead agent, shown in the left pane. It is not tie - **Signals.** Every published event goes through `Manager.Observe`. These become signals in `lead_inputs` for every user who has a lead session: `inbox.created` (a question with its choices and recommendation, an error or rate limit), `inbox.updated` when a human answered a question or error (a question's signal also carries its text and the agent's recommendation, so the lead sees where the human departed from it), `task.created` (naming the TODO item it was started from, or a subtask's parent, if any), `task.merge_started` (" is merging, started by ."), `task.merged`, `task.discarded`, `turn.ended` with `summary: true` (" finished a turn:" with its `result` cut to 1500 characters), `task.workflow_stage` (" started stage "" of workflow "".") and `task.workflow_finished` (" finished workflow "":" with its output cut to 1500 characters), `review.started` (" started a review of snapshot ."), `review.finished` (" review of snapshot :" with the review's summary, `change` and findings, or why it failed or was skipped; `reviewLine` in `internal/agents/review.go`), `user.message` on a task from a human (see "Pane lock"), and `task.state` (" state: → .", or " state: ." for a new task; see "Task state in the store"). The store hands `Observe` all of one transaction's events together, and they become one signal, its `task.state` lines first, so a turn's summary always arrives with the state its turn end left the task in. The human switching the right pane is not a signal. A signal caused by the lead itself (actor ` (lead)`) is skipped, as is the update from an answer it gave. A task's `task.created` event carries the actor that created it, separately from its `created_by` login: tasks the lead creates with `create_task` or `start_todo` keep the human's login as `created_by` but have actor ` (lead)`. Whenever the lead is idle, byre sends the oldest held message from the human as its own turn, or else every held signal as one `byre` message headed "Signals since your last turn (n)", followed by "Tasks now:" with the `list_tasks` text and a line saying what the lead's right pane shows (task or project, and selected tab) and whether the human is working in it (`leadStateText` in `internal/agents/lead.go`). If collecting that fails, for example because a project's repository is gone, the batch is still sent, ending instead with "The task list and right pane state are unavailable:" and the error, which is also logged as `lead state`. Its guidance tells it not to respond to the human's own actions, and to make its final message exactly `NO_REPLY` when a batch needs nothing from it. A transcript (`quietReply` in `web/src/task.ts`) never draws an `agent.message` whose last line is `NO_REPLY` (the model sometimes writes a sentence before it), and hides the live partial message while its text is still a prefix of it; the partial message starts hidden and is first shown by its 100ms render, so an empty one never takes up space. A lead turn that byre starts without a handoff (a signal batch or a handoff continuation) is held (`ctx.held`): its `byre` message is never drawn, and the turn isn't opened, so the scanner stays off, and its partial message stays hidden, until the lead draws something else, such as a tool call, thinking or a real message. When such a turn ends in `NO_REPLY`, everything it drew is removed with it (tool calls, thinking), so the transcript keeps nothing of it; a turn that ends in a real message keeps all of it. - **Tools.** `list_tasks` (each project's active tasks with their state, each subtask noting its parent, then a line listing the titles of its `TODO.md` items on the default branch that no active task was started from), `read_transcript` (a task's events in brief, the last `limit` or those after `after_seq`; closed tasks too), `search_closed_tasks` (see "Closing a task"), `read_inbox`, `answer_question` (an `option`, `other` or `dismiss` response recorded as the user, through the same checks as `POST /api/inbox/responses`), `send_message` (starts the task's agent if it has none; refused while it is mid-turn), `create_task` and `start_todo` (each with an optional `workflow` to freeze and run), `interrupt`, `compact_task`, `merge_task` (optional `target`: `main`, or `parent` for a subtask, see "Subtasks"), `discard_task`, `show`, `compact_self`, the note tools (see "Notes") and the workflow tools (see "Workflows"). The lead also has Claude Code's own tools and reads worktrees by path. Messages it sends task agents have actor ` (lead)`, so they don't make the user a participant; tasks, merges, discards and answers are done as the user. Its guidance says to answer a question only when the answer is clear or the human gave it, and to merge or discard only when the human asks. - **Focus.** `users.focus` holds what the user's right pane shows: `project_id`, `task_id`, `tabs` (the tabs put first, in order) and `tab` (the selected one). With a `task_id`, `project_id` is always that task's project, filled in by the server; with only a `project_id`, the pane shows that project with no task, which only the Project tab has anything for. `GET /api/me/focus` reads it; `PUT /api/me/focus` and the lead's `show` set it, refusing an unknown tab, a task that isn't active or an unknown project, and record a `focus.updated` event owned by the user. The page applies the focus on load and on every `focus.updated` it didn't cause itself; the human's own changes (⌘K, the right pane's task picker, clicking a tab, a task link, a notification) are sent with `PUT /api/me/focus`. The URL follows the focus: `#/task/`, `#/project/`, or `#/` for neither. -- **Pane lock.** A `show` of what the pane already shows (the same task, or the same project with no task, on the same selected tab; `sameView`) tells the lead so and applies only its `tabs` order, whether or not the human is in the pane, so the human's selected tab stays selected. Otherwise, while the human is working in the right pane, the lead's `show` is refused with an error telling it to say in chat what it would have shown, unless its selected tab is `project` (the fallback the lead shows when nothing needs the human, so the human being there doesn't hold it) or the pane is finished (`Manager.paneFinished`): its task is merged, discarded or merging (`merging_by` set), or its selected tab is `inbox` and the task has no open items, or the human is waiting on the task's agent (`Manager.paneWaiting`): the selected tab is `transcript`, the task's implementer session is running, the page reports no unsent text in the pane, and either the `user.message` that started the running turn came from the user after the pane last moved, or the pane last moved 10 seconds ago or more (`watchHold`). The pane's last move is the last `PUT /api/me/focus` or `show` that set it (`Manager.paneMoved`, in memory, so a restart counts as long ago). For the user's own message, unsent text reported before the turn started doesn't count, since the page's report of the composer emptying on send can arrive after the message does. A `user.message` on a task from a human (not `byre`, an agent or a lead) signals every lead " sent a message; its agent is working on it.", and the sender's own lead, when its pane shows that task and is waiting that way, also gets "Your right pane is free: the human is waiting on 's agent." Each `PUT /api/me/focus` from the user starts a 10-second timer (`endWatchHold`); when it fires, if the pane hasn't moved since, the page still reports the human in it, the user hasn't messaged the task since the move (that already freed it, above), and the pane is waiting that way, the lead gets the signal "Your right pane is free: the human has watched 's agent work for 10 seconds without typing." The lead's own `show` starts no timer. When an `inbox.updated`, `task.merge_started`, `task.merged` or `task.discarded` event leaves a user's focused task finished that way, that user's signal for it gets a line "Your right pane is free: has nothing left in its inbox." (or "is merging."/"is merged."/"is discarded."), even when the event itself makes no signal, such as a dismissed error; `inbox.updated` on a task that is no longer active adds none. The lead's guidance tells it to show the next thing that needs the human on that line, or, when nothing does, the project's Project tab with a short status in chat: one line per active task, then up to three TODO items it would start next, each with a reason, starting one only when the human picks it. It writes each item as a link to `#/todo//`; Markdown views draw that (`web/src/todolink.ts`) as the title followed by small accent-coloured `start · discuss` text links (borderless buttons) that call `POST /api/projects/{id}/todo/tasks` with the first model, as the Project tab's do, or, once an active task was started from that item, the task's colour, dot and state, linking to it. A chat view refills them whenever its transcript grows or tasks or sessions change. When that status says nothing new since its last one, it replies `NO_REPLY` instead. The page sends `PUT /api/me/pane-focus` with `focused` and `drafting` whenever either changes: `focused` is true while the browser window has focus and the focused element is inside the right pane, and `drafting` while any textarea in the right pane (other than xterm's helper) holds non-blank text, checked on focus changes, `input` and `submit`. The pane itself is focusable (`tabIndex` -1), so a click anywhere in it counts, not only on inputs and buttons; a re-render that removes the focused element hands focus back to the pane rather than counting as leaving. The page sends false on `pagehide` (with `keepalive`) and resends true when its event stream reconnects. byre keeps both flags and the time of the last report in memory per user (`Manager.paneFocus`, `Manager.paneReported`), so they start false after a restart until the page resends them. +- **Pane lock.** A `show` of what the pane already shows (the same task, or the same project with no task, on the same selected tab; `sameView`) tells the lead so and applies only its `tabs` order, whether or not the human is in the pane, so the human's selected tab stays selected. Otherwise, while the human is working in the right pane, the lead's `show` is refused with an error telling it to say in chat what it would have shown, unless its selected tab is `project` (the fallback the lead shows when nothing needs the human, so the human being there doesn't hold it) or the pane is finished (`Manager.paneFinished`): its task is merged, discarded or merging (`merging_by` or `finalizing` set), or its selected tab is `inbox` and the task has no open items, or the human is waiting on the task's agent (`Manager.paneWaiting`): the selected tab is `transcript`, the task's implementer session is running, the page reports no unsent text in the pane, and either the `user.message` that started the running turn came from the user after the pane last moved, or the pane last moved 10 seconds ago or more (`watchHold`). The pane's last move is the last `PUT /api/me/focus` or `show` that set it (`Manager.paneMoved`, in memory, so a restart counts as long ago). For the user's own message, unsent text reported before the turn started doesn't count, since the page's report of the composer emptying on send can arrive after the message does. A `user.message` on a task from a human (not `byre`, an agent or a lead) signals every lead " sent a message; its agent is working on it.", and the sender's own lead, when its pane shows that task and is waiting that way, also gets "Your right pane is free: the human is waiting on 's agent." Each `PUT /api/me/focus` from the user starts a 10-second timer (`endWatchHold`); when it fires, if the pane hasn't moved since, the page still reports the human in it, the user hasn't messaged the task since the move (that already freed it, above), and the pane is waiting that way, the lead gets the signal "Your right pane is free: the human has watched 's agent work for 10 seconds without typing." The lead's own `show` starts no timer. When an `inbox.updated`, `task.merge_started`, `task.merged` or `task.discarded` event leaves a user's focused task finished that way, that user's signal for it gets a line "Your right pane is free: has nothing left in its inbox." (or "is merging."/"is merged."/"is discarded."), even when the event itself makes no signal, such as a dismissed error; `inbox.updated` on a task that is no longer active adds none. The lead's guidance tells it to show the next thing that needs the human on that line, or, when nothing does, the project's Project tab with a short status in chat: one line per active task, then up to three TODO items it would start next, each with a reason, starting one only when the human picks it. It writes each item as a link to `#/todo//`; Markdown views draw that (`web/src/todolink.ts`) as the title followed by small accent-coloured `start · discuss` text links (borderless buttons) that call `POST /api/projects/{id}/todo/tasks` with the first model, as the Project tab's do, or, once an active task was started from that item, the task's colour, dot and state, linking to it. A chat view refills them whenever its transcript grows or tasks or sessions change. When that status says nothing new since its last one, it replies `NO_REPLY` instead. The page sends `PUT /api/me/pane-focus` with `focused` and `drafting` whenever either changes: `focused` is true while the browser window has focus and the focused element is inside the right pane, and `drafting` while any textarea in the right pane (other than xterm's helper) holds non-blank text, checked on focus changes, `input` and `submit`. The pane itself is focusable (`tabIndex` -1), so a click anywhere in it counts, not only on inputs and buttons; a re-render that removes the focused element hands focus back to the pane rather than counting as leaving. The page sends false on `pagehide` (with `keepalive`) and resends true when its event stream reconnects. byre keeps both flags and the time of the last report in memory per user (`Manager.paneFocus`, `Manager.paneReported`), so they start false after a restart until the page resends them. - **Start hold.** Start on a TODO item in the Project tab leaves the pane where it is. When the user starts one themselves (`StartTodo` with the user as actor, not the lead's `start_todo`), byre records the time in `Manager.startedTodo` for them, and for 10 seconds after it (`startHold`), while the pane's selected tab is `project`, every `show` that would change the view is refused, focused or not, with an error telling the lead the human is likely to start more. The user's own `PUT /api/me/focus` (switching task or tab) or a message to the lead clears it. The refusal and the `show` tool's description both tell the lead the hold lasts 10 seconds. When the 10 seconds pass without it being cleared or restarted by another Start (`endStartHold`, on a timer), byre clears it and, if the pane's selected tab is still `project`, sends the lead the signal "The start hold is over: …" so it can show what it held back; its guidance tells it to reply `NO_REPLY` when nothing was. It is in memory, so a restart clears it. - **Compaction.** The pane's **Compact** button asks the lead for a handoff (`leadHandoffPrompt`). The lead can't see its own context size, so the first time one of its turns ends (other than a handoff or a `compact_self` turn) with the latest `session.context` `used` at half its `window` or more, byre holds a signal for it giving the percentage and asking it to call `compact_self` at the next logical point (`warnLeadContext` in `internal/agents/lead.go`); it is sent once per live process, so a byre restart can send it again once the window is known. The lead calls `compact_self`; when that turn ends normally, byre sends the same request. The handoff turn ends the session, and a new lead session starts from `leadHandoffPreamble` plus the handoff; held input follows once that turn ends. - **Notes.** The lead keeps notes on how its user works (priorities, preferences, the reasoning behind choices) in `lead_notes`, one row per note keyed by `owner`, through its `remember` (add, or replace the text of an `id`), `forget` and `list_notes` tools (`internal/agents/leadnotes.go`). Each change records a `lead.note_saved` or `lead.note_deleted` event owned by the user. When a lead's agent process starts, `notesText` lists every note with its id after `leadGuidance` in the session's instructions (`--append-system-prompt` for Claude, which keeps what the session's first request carried; `developerInstructions` for Codex), but not in the per-turn guidance reminder, so a new or compacted lead session starts from the current notes and notes written mid-session are already in its context. The guidance tells the lead when to write a note (an answer that departs from a recommendation, a correction, a stated preference), to cite a note's id when it shapes a recommendation, and that it may answer a task agent's question itself when a note settles it outright. `GET /api/me/lead/notes` lists the caller's notes and `DELETE /api/me/lead/notes/{noteId}` deletes one (`Manager.DeleteLeadNote`), and, when the user has a live lead session, holds a message for it from `byre` naming the note's id and text and telling it to stop applying it, since the running session still has the note in its instructions. It is held as a message, not a signal, so it keeps its place in line with the human's own messages: it goes out ahead of any message they send after the delete, as its own turn (drawn like a signal batch's, and folded away on `NO_REPLY`) or mid-turn after the lead's next tool call; the Settings tab lists them with a delete link each, refetching on either event. `sqlite3 byre.db "SELECT owner, id, text FROM lead_notes ORDER BY created_at"` shows them. diff --git a/internal/agents/leadtools.go b/internal/agents/leadtools.go index d217679..fc1bd59 100644 --- a/internal/agents/leadtools.go +++ b/internal/agents/leadtools.go @@ -531,11 +531,7 @@ func (m *Manager) paneFinished(ctx context.Context, f api.Focus) (string, error) if task.Status != api.TaskStatusActive { return "is " + string(task.Status), nil } - mergingBy, err := m.store.MergingBy(ctx, task.Id) - if err != nil { - return "", err - } - if mergingBy != "" { + if task.Merging { return "is merging", nil } if f.Tab != api.TabInbox { diff --git a/internal/agents/manager.go b/internal/agents/manager.go index ebb2591..2edf57b 100644 --- a/internal/agents/manager.go +++ b/internal/agents/manager.go @@ -283,6 +283,11 @@ func (m *Manager) Recover(ctx context.Context) error { return err } + err = m.recoverFinalizing(ctx) + if err != nil { + return err + } + staged, err := m.recoverStages(ctx) if err != nil { return err @@ -365,7 +370,7 @@ func (m *Manager) failResume(ctx context.Context, sess api.AgentSession, task ap if derr != nil { return derr } - next, serr := m.store.EndTurn(ctx, sess.Id, "byre", map[string]any{"error": err.Error()}, nil, false) + next, _, serr := m.store.EndTurn(ctx, sess.Id, "byre", map[string]any{"error": err.Error()}, nil, false, false) if serr != nil { return serr } @@ -694,7 +699,7 @@ func (m *Manager) pump(l *live) { slog.Error("drop stage output", "session", l.id, "err", err) } } - next, err := m.store.EndTurn(ctx, l.id, agentActor, map[string]any{"error": "agent process exited mid-turn"}, nil, false) + next, _, err := m.store.EndTurn(ctx, l.id, agentActor, map[string]any{"error": "agent process exited mid-turn"}, nil, false, false) if err != nil { slog.Error("record session exit", "session", l.id, "err", err) } @@ -930,7 +935,7 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error { } turn := m.event(l, agentActor, "turn.ended", payload) - next, err := m.store.EndTurn(ctx, l.id, agentActor, nil, &turn, review) + next, finalizeBy, err := m.store.EndTurn(ctx, l.id, agentActor, nil, &turn, review, finished && !handoff) if err != nil { return err } @@ -957,12 +962,13 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error { go m.continueFromHandoff(l.id, res.result) return nil } + if finalizeBy != "" { + m.logUndelivered(ctx, l.id, "turn ended in merge") + go m.finishMerge(l, finalizeBy) + return nil + } mergeBy := m.takeMerge(l.taskID) 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) if !res.isError || interrupted { diff --git a/internal/agents/mergein.go b/internal/agents/mergein.go index ddc0dba..10cce87 100644 --- a/internal/agents/mergein.go +++ b/internal/agents/mergein.go @@ -135,6 +135,27 @@ func (m *Manager) recoverMerges(ctx context.Context) error { return nil } +func (m *Manager) recoverFinalizing(ctx context.Context) error { + finalizing, err := m.store.FinalizingTasks(ctx) + if err != nil { + return err + } + + for _, t := range finalizing { + err = m.store.ClearFinalizing(ctx, t.Id) + if err != nil { + return err + } + sessions, err := m.store.ListSessions(ctx, t.Id) + if err != nil { + return err + } + slog.Error("merge was finishing at startup", "task", t.Id) + m.fileTaskError(ctx, t, sessions, "byre restarted while finishing this task's merge, so it may not have been pushed, or the task's worktree may be gone. Check the default branch before merging again.") + } + return nil +} + func (m *Manager) startHeldMerges(ctx context.Context) { projects, err := m.store.ProjectsWithQueuedMerges(ctx) if err != nil { @@ -253,6 +274,10 @@ func (m *Manager) finishMerge(l *live, login string) { } slog.Error("finish merge", "task", l.taskID, "err", err) + cerr := m.store.ClearFinalizing(ctx, l.taskID) + if cerr != nil { + slog.Error("clear merge finalizing", "task", l.taskID, "err", cerr) + } m.fileError(l, "Merging the task failed: "+err.Error()) err = m.Deliver(ctx, l.id) if err != nil { diff --git a/internal/agents/subtasks.go b/internal/agents/subtasks.go index 9f1df01..747c9eb 100644 --- a/internal/agents/subtasks.go +++ b/internal/agents/subtasks.go @@ -320,11 +320,7 @@ func (m *Manager) MergeIntoParent(ctx context.Context, task api.Task, user api.U if found && sess.State == api.AgentSessionStateRunning { return api.Task{}, fmt.Errorf("%w: subtask %s's agent is mid-turn; wait for the turn to end or interrupt it", ErrConflict, task.Id) } - mergingBy, err := m.store.MergingBy(ctx, task.Id) - if err != nil { - return api.Task{}, err - } - if mergingBy != "" || task.MergeQueuedAt != nil { + if task.Merging || task.MergeQueuedAt != nil { return api.Task{}, fmt.Errorf("%w: subtask %s is merging into the default branch", ErrConflict, task.Id) } @@ -332,7 +328,7 @@ func (m *Manager) MergeIntoParent(ctx context.Context, task api.Task, user api.U switch { case errors.Is(err, store.ErrNotFound): return api.Task{}, fmt.Errorf("%w: subtask %s is closed", ErrConflict, task.Id) - case errors.Is(err, store.ErrDiscardPending), errors.Is(err, store.ErrParentMergeRequested): + case errors.Is(err, store.ErrDiscardPending), errors.Is(err, store.ErrMerging), errors.Is(err, store.ErrParentMergeRequested): return api.Task{}, fmt.Errorf("%w: subtask %s: %w", ErrConflict, task.Id, err) case err != nil: return api.Task{}, err diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index 61e0db1..290dbb3 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -1080,7 +1080,7 @@ type Task struct { // 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"` - // Merging Whether a Merge was started and its turn hasn't ended yet. + // Merging Whether a Merge was started and hasn't finished: its turn is running, or the turn ended and byre is squashing, pushing and closing the task. Merging bool `json:"merging"` // MergingIntoParent Whether a merge of this subtask into its parent was requested and the parent hasn't merged it yet. @@ -3339,74 +3339,74 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ "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", @@ -3427,127 +3427,127 @@ var swaggerSpec = []string{ "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/store/discard.go b/internal/store/discard.go index 908fe74..c74ae50 100644 --- a/internal/store/discard.go +++ b/internal/store/discard.go @@ -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 { diff --git a/internal/store/store.go b/internal/store/store.go index 2676b32..5bc3961 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -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` diff --git a/web/src/api.gen.ts b/web/src/api.gen.ts index 6d8cc68..b8b47b0 100644 --- a/web/src/api.gen.ts +++ b/web/src/api.gen.ts @@ -987,7 +987,7 @@ export interface components { * @enum {string} */ state: "running" | "idle" | "review_pending" | "reviewing" | "merging" | "discarding" | "merged" | "discarded" | "no_agent"; - /** @description Whether a Merge was started and its turn hasn't ended yet. */ + /** @description Whether a Merge was started and hasn't finished: its turn is running, or the turn ended and byre is squashing, pushing and closing the task. */ merging: boolean; /** @description Whether a Discard was started and the task isn't closed yet: its summary turn is waiting for the running turn, running, or done and the task is being removed. */ discarding: boolean;