diff --git a/TODO.md b/TODO.md index d8132ce..b582388 100644 --- a/TODO.md +++ b/TODO.md @@ -17,8 +17,6 @@ # M7 — Inbox triage -- Take both paths when unattended - An agent working unattended (no human to ask) that hits a question with two plausible answers forks at it: the task carries on with one answer and a fork, started the same way as answering again, carries on with the other, so the human later compares both results instead of the agent guessing. # M8 — macOS client diff --git a/api/openapi.yaml b/api/openapi.yaml index 264206a..d528330 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -61,6 +61,23 @@ paths: schema: { $ref: "#/components/schemas/User" } "400": { $ref: "#/components/responses/Error" } + /api/me/away: + put: + operationId: updateAway + description: Sets whether the calling user is away. While away, byre answers every question an agent asks in the user's tasks at once, telling it to pick the most likely answer, and turning it on answers the questions already open. + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/Away" } + responses: + "200": + description: The updated user. + content: + application/json: + schema: { $ref: "#/components/schemas/User" } + "400": { $ref: "#/components/responses/Error" } + /api/me/review-models: put: operationId: updateReviewModels @@ -1189,7 +1206,7 @@ components: User: type: object - required: [login, display_name, git_name, git_email, notification_prefs, default_model, review_model, second_review_model] + required: [login, display_name, git_name, git_email, notification_prefs, default_model, review_model, second_review_model, away] properties: login: { type: string } display_name: { type: string } @@ -1199,6 +1216,13 @@ components: default_model: { type: string, description: The model the app starts new tasks on. } review_model: { type: string, description: The model that reviews this user's tasks, unless it wrote the work under review. } second_review_model: { type: string, description: The model that reviews this user's tasks when review_model wrote the work under review. } + away: { type: boolean, description: "While true, byre answers the questions agents ask in this user's tasks at once." } + + Away: + type: object + required: [away] + properties: + away: { type: boolean } DefaultModel: type: object @@ -1568,9 +1592,9 @@ components: event_seq: { type: integer, format: int64, description: "The task's latest event when the snapshot was started: the tree holds at least the work up to it." } trigger: type: string - enum: [message, turn_end, tool_call, ask_human, timer, requested, answer] - x-enum-varnames: [SnapshotTriggerMessage, SnapshotTriggerTurnEnd, SnapshotTriggerToolCall, SnapshotTriggerAskHuman, SnapshotTriggerTimer, SnapshotTriggerRequested, SnapshotTriggerAnswer] - description: "turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. answer: just before answers from the human were handed to the agent. message (the agent wrote text after one or more tool calls), tool_call, ask_human and timer appear only on snapshots taken by earlier builds." + enum: [message, turn_end, tool_call, ask_human, timer, requested, answer, fork] + x-enum-varnames: [SnapshotTriggerMessage, SnapshotTriggerTurnEnd, SnapshotTriggerToolCall, SnapshotTriggerAskHuman, SnapshotTriggerTimer, SnapshotTriggerRequested, SnapshotTriggerAnswer, SnapshotTriggerFork] + description: "turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. answer: just before answers from the human were handed to the agent. fork: when the task's agent forked the task with its fork tool. answer and fork snapshots are never reviewed. message (the agent wrote text after one or more tool calls), tool_call, ask_human and timer appear only on snapshots taken by earlier builds." created_at: { type: string, format: date-time } Preview: diff --git a/docs/operations.md b/docs/operations.md index 6514fbc..769c2b1 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -165,7 +165,7 @@ Clients reach `tailscale serve` over HTTP/2; it proxies to byre over HTTP/1.1 an **Building.** `mac/bundle.sh`, from any directory, builds the release configuration with `swift build` in `~/Library/Caches/byre/build`, one build directory every worktree shares and takes turns in (`docs/macos.md`, "Stack", says why), and assembles `mac/.build/Byre.app`: the `Byre` executable in `Contents/MacOS`; the server, built with `go build ./cmd/byre`, `byre-hands`, built from `hands/`, and `byre-update`, the `mac/` package's second executable, in `Contents/Helpers` (not beside `Byre`: the default APFS volume ignores case, so `byre` and `Byre` would be one file); Sparkle's framework in `Contents/Frameworks`; `mac/Info.plist` with a `ByreCommit` key added holding the worktree's `git rev-parse HEAD`, `CFBundleShortVersionString` its `git describe --tags --always` without the leading `v` (`0.1.0` on a release tag, `0.1.0-12-gabc1234` after it) and `CFBundleVersion` its `git rev-list --count HEAD` (the About panel shows them as "Version ()", and Sparkle compares the count); the `mac/fonts/` woff2 files, an icon rendered from `mac/icon.svg` with `rsvg-convert` and `iconutil` (cached as `mac/.build/byre.icns` until the SVG changes) and `mac/byre.provisionprofile`. It signs each piece with the Developer ID Application certificate, a secure timestamp and the hardened runtime, innermost first: `byre-hands` as `run.fc.byre.hands` and the server as `run.fc.byre`, both with `mac/automation.entitlements` (Apple Events, which the hardened runtime otherwise refuses), Sparkle's `Autoupdate`, `Updater.app` and framework (its XPC services, only needed by sandboxed apps, are removed), `byre-update` as `run.fc.byre.update`, then the bundle with `mac/byre.entitlements`. It prints the bundle's path. It needs Xcode selected (`xcode-select --print-path` prints `/Applications/Xcode.app/Contents/Developer`), Go, `rsvg-convert` (Homebrew's `librsvg`), the team's Developer ID Application certificate and network access for the timestamp, and it stops first unless `mac/byre.provisionprofile` has `ProvisionsAllDevices` (a Developer ID profile; a development profile can't authorize the Developer ID signature); `docs/macos.md`, "Signing", covers the certificate and profile. Debug builds are refused by an `#error` in `ByreApp.swift`. The first build on a Mac fetches the Swift packages pinned in `mac/Package.resolved` and compiles them and the generated API client; later builds, from any worktree, recompile only the targets whose sources differ from the last build's, usually just `Byre`. The API client is generated from `api/openapi.yaml` at build time. The palette is `mac/Sources/Byre/Palette.swift`, one light and one dark value per colour. -**Installing.** A release is installed from its disk image into `/Applications` (see "Releases") and updates itself (`docs/macos.md`, "Updates"). A deploy installs its build there the same way (see "Releases"). A build from a checkout can also run from where it was built, `open mac/.build/Byre.app` in the worktree that built it, after quitting the running app (`pkill -x Byre` quits every running copy). Every build shares the bundle identifier `run.fc.byre.mac` and the same store, so any build reconnects to the servers the last one saved, switched between with the window's server picker. Closing the window leaves the app running for notifications; ⌘Q quits it. +**Installing.** A release is installed from its disk image into `/Applications` (see "Releases") and updates itself (`docs/macos.md`, "Updates"). A deploy installs its build there the same way (see "Releases"). A build from a checkout can also run from where it was built, `open mac/.build/Byre.app` in the worktree that built it, after quitting the running app (`pkill -x Byre` quits every running copy). Every build shares the bundle identifier `run.fc.byre.mac` and the same store, so any build reconnects to the servers the last one saved, switched between with the window's server picker in its compact toolbar, which ends with the Away button (see "Away"). Closing the window leaves the app running for notifications; ⌘Q quits it. **Storage.** The app keeps three kinds of record in SwiftData (`mac/Sources/Byre/Persistence.swift`): each saved server's origin and window colour, when each task was last selected, and composer and answer drafts as JSON, one record per server and login (`docs/macos.md`, "Persistence" and "Drafts"). The store is `~/Library/Application Support/run.fc.byre.mac/byre.store`, with its `-shm` and `-wal` files and CloudKit's working files beside it in that directory; the app isn't sandboxed, so it names its own file rather than SwiftData's default `default.store`, which every unsandboxed app shares. The store syncs through the iCloud private database of container `iCloud.run.fc.byre.mac`, in CloudKit's Production environment (`com.apple.developer.icloud-container-environment` in `mac/byre.entitlements`), whose schema is deployed from Development in the CloudKit console before a release that changes the SwiftData models; the CloudKit console (icloud.developer.apple.com, the container, Production, Private Database, zone `com.apple.coredata.cloudkit.zone`) shows the records. Deleting the local files while the app isn't running makes the next launch download the records again from iCloud; deleting the records in the console as well starts from the connection screen. Pasted images stay in memory and are dropped on Forget Server and quit. The app logs through `os.Logger`: `log show --last 10m --predicate 'subsystem == "run.fc.byre.mac"'` prints it. @@ -334,12 +334,16 @@ An `other` response, or a pick-any `option` one, may carry `images`, blob ids fr A submission is all or nothing. `Store.Respond` resolves each item with an `UPDATE` conditioned on `state = 'open'` in one transaction, so when any item was answered, dismissed or withdrawn by someone else in the meantime (a second tab, the lead's `answer_question`, the agent's `cancel_question`), the whole submission is rolled back and refused with 409, and no response is recorded. -Answers reach the agent as one message holding every undelivered `option` and `other` response of the session (`deliveryMessage` in `internal/agents/inbox.go`), never while one of its questions is still open and unanswered (`AwaitingHuman` in `internal/store/inbox.go`); until then they stay held (`inbox_responses.delivered_at` empty). While a turn is running, the task session's PostToolUse hook (`afterTool` in `internal/agents/snapshots.go`, through `deliverMidTurn`) hands them over after the agent's next tool call as the hook's `additionalContext`, after any queued messages (see "Agent sessions"), so the agent reads them alongside that tool's result without being interrupted (for Antigravity, the `PreInvocation` hook hands them over as a user message before the model's next call after the first; for Codex, after each completed tool item, `deliverMidTurn` hands them over with `turn/steer` instead; when the steer fails, `restoreMidTurn` puts the taken queued messages back under their original ids and clears the answers' `delivered_at` and `snapshot_n`, so `Deliver` sends them when the turn ends, and logs `mid-turn delivery restored`); the `user.message` event recording it carries `mid_turn: true` and no `head`, and the transcript doesn't treat it as a turn start. A turn that makes no further tool call leaves them to `Deliver`, which runs after each submission and when a turn ends, and sends them as a new turn once the session is `idle`. Just before either hands answers over, byre snapshots the task's worktree (`snapshotForAnswer` in `internal/agents/snapshots.go`, trigger `answer`), synchronously and under the task's mutex, and records the snapshot's `n` on each answer it hands over (`inbox_responses.snapshot_n`, `snapshot_n` in the API), so the snapshot's tree and `event_seq` give the worktree and the conversation as they stood before the agent read the answer. A new snapshot is taken for every hand-over, even when the tree is unchanged, so each one's `event_seq` and session are the conversation boundary just before that answer and `POST /api/tasks/{taskId}/snapshots/{n}/fork` on it continues from there. An `answer` snapshot is never reviewed. Everything that looks for the task's latest snapshot to judge review state skips `answer` ones (`LastSnapshotNot`): the dedupe of the other triggers, which skip a snapshot only when its tree equals the last non-`answer` one's, so work an `answer` snapshot caught mid-turn is still snapshotted and reviewed when the turn ends; Merge after review; and the Merge confirmation's review warnings. A Merge turn already holds the task's mutex, so mid-turn delivery in it snapshots without taking it again; a worktree with merge conflicts snapshots with the conflict markers as they stand, and its real index is left untouched. No snapshot is taken once the task is closing, or when taking it fails; the answers then have no `snapshot_n`. A failure is logged as `take snapshot` and recorded as a `session.error` in the transcript, and the answers are still handed over: holding them would leave an idle agent waiting with nothing to retry the delivery. The log traces each answer: `inbox response recorded` when it is stored, `deliver held` with the reason, `deliver sent mid-turn` with the snapshot, `deliver sent` with the session's state, the snapshot and whether its agent process had exited or byre was closing, and `undelivered responses` with a reason wherever a session has held answers when its turn ends in a handoff or merge, its agent process ends, startup resumes it, or startup finds it idle. `sqlite3 byre.db "SELECT * FROM inbox_responses WHERE delivered_at IS NULL"` lists the answers held now. +Answers reach the agent as one message holding every undelivered `option` and `other` response of the session (`deliveryMessage` in `internal/agents/inbox.go`), never while one of its questions is still open and unanswered (`AwaitingHuman` in `internal/store/inbox.go`); until then they stay held (`inbox_responses.delivered_at` empty). While a turn is running, the task session's PostToolUse hook (`afterTool` in `internal/agents/snapshots.go`, through `deliverMidTurn`) hands them over after the agent's next tool call as the hook's `additionalContext`, after any queued messages (see "Agent sessions"), so the agent reads them alongside that tool's result without being interrupted (for Antigravity, the `PreInvocation` hook hands them over as a user message before the model's next call after the first; for Codex, after each completed tool item, `deliverMidTurn` hands them over with `turn/steer` instead; when the steer fails, `restoreMidTurn` puts the taken queued messages back under their original ids and clears the answers' `delivered_at` and `snapshot_n`, so `Deliver` sends them when the turn ends, and logs `mid-turn delivery restored`); the `user.message` event recording it carries `mid_turn: true` and no `head`, and the transcript doesn't treat it as a turn start. A turn that makes no further tool call leaves them to `Deliver`, which runs after each submission and when a turn ends, and sends them as a new turn once the session is `idle`. Just before either hands answers over, byre snapshots the task's worktree (`snapshotForAnswer` in `internal/agents/snapshots.go`, trigger `answer`), synchronously and under the task's mutex, and records the snapshot's `n` on each answer it hands over (`inbox_responses.snapshot_n`, `snapshot_n` in the API), so the snapshot's tree and `event_seq` give the worktree and the conversation as they stood before the agent read the answer. A new snapshot is taken for every hand-over, even when the tree is unchanged, so each one's `event_seq` and session are the conversation boundary just before that answer and `POST /api/tasks/{taskId}/snapshots/{n}/fork` on it continues from there. An `answer` snapshot is never reviewed, nor is a `fork` one (see "Forking"). Everything that looks for the task's latest snapshot to judge review state skips both (`LastReviewable`): the dedupe of the other triggers, which skip a snapshot only when its tree equals the last non-`answer` one's, so work an `answer` snapshot caught mid-turn is still snapshotted and reviewed when the turn ends; Merge after review; and the Merge confirmation's review warnings. A Merge turn already holds the task's mutex, so mid-turn delivery in it snapshots without taking it again; a worktree with merge conflicts snapshots with the conflict markers as they stand, and its real index is left untouched. No snapshot is taken once the task is closing, or when taking it fails; the answers then have no `snapshot_n`. A failure is logged as `take snapshot` and recorded as a `session.error` in the transcript, and the answers are still handed over: holding them would leave an idle agent waiting with nothing to retry the delivery. The log traces each answer: `inbox response recorded` when it is stored, `deliver held` with the reason, `deliver sent mid-turn` with the snapshot, `deliver sent` with the session's state, the snapshot and whether its agent process had exited or byre was closing, and `undelivered responses` with a reason wherever a session has held answers when its turn ends in a handoff or merge, its agent process ends, startup resumes it, or startup finds it idle. `sqlite3 byre.db "SELECT * FROM inbox_responses WHERE delivered_at IS NULL"` lists the answers held now. ### Errors An agent error (a turn that ends in error without a human interrupt, a process that dies mid-turn, a failed commit, merge, cleanup or workflow step) is recorded as a `session.error` event on the session (`recordError` in `internal/agents/inbox.go`, actor `byre`, payload `error`), and a rate-limited turn as `session.rate_limited` (`recordRateLimited`, payload `vendor`, `rate_limit` and `error`). The transcript draws each as an error note, a rate limit with its vendor, limit type and reset time. Nothing answers or dismisses them: the human reacts by messaging the agent. On a task they signal the lead and can notify (below). +### Away + +A user who is away has byre answer their agents' questions instead of waiting (`internal/agents/away.go`). `users.away`, `away` on `GET /api/me`, is set with `PUT /api/me/away` (`Manager.SetAway`), which records `user.updated`. While it is on, every question filed by `ask_human` in a task the user created is answered at once, as that user, with an `other` response of `awayText`: pick the most likely answer and carry on; fork with the `fork` tool (see "Forking") only when strongly convinced both answers are worth following; don't guess at what only the user can give (a test result, a password, a permission), and carry on with other work or end the turn instead; and no authorization is granted. The tool's result says so, and the answer is delivered like any other, usually mid-turn right after the `ask_human` call, so it gets an `answer` snapshot and the user can **Answer again** with their own answer when back. Turning it on answers the user's tasks' questions already open the same way; turning it off lets new questions wait again. The app's Away button, at the end of the server picker in the window's toolbar, shows on (accent) when the user is away on any connected server and sets all of them to the opposite on click, so a mixed state is always cleared by one click. `sqlite3 byre.db "SELECT login FROM users WHERE away = 1"` lists who is away. + ## Notifications The running macOS app posts notifications for events on its SSE stream, raised by the app itself (`notifyInbox`, `notifyError`, `notifyTurn` and `notifyTask` in `mac/Sources/Byre/Notifications.swift`, through `UNUserNotificationCenter`). With the app not running, nothing notifies; closing its window leaves it running. @@ -439,7 +443,7 @@ The transcript is the timeline. The UI learns a task's snapshots from the `snaps `POST /api/tasks/{taskId}/snapshots/{n}/fork` (the **fork** button on a snapshot's transcript row) starts a new task from a snapshot (`internal/agents/fork.go`). Its branch and worktree start from the snapshot's commit, so the fork's worktree holds exactly the snapshot's tree, uncommitted work of the time included, as the branch's first commit. The task is titled ` (fork of #n)`, keeps the source's `merge_title` and `todo_item`, and records `forked_from` (the source task) and `forked_at` (n) in the `tasks` columns of those names. It gets an idle session, created by the requesting user (who needs a git identity), on the snapshot session's model, with `forked_from` set to that session's `vendor_session_id` and `forked_at` to the cut point below. Nothing runs until the first message is sent to it; that turn starts Claude Code with `--resume=<forked_from> --resume-session-at=<forked_at> --fork-session --session-id=<new id>`, or Codex with `thread/fork` (`threadId` `forked_from`, `lastTurnId` `forked_at`) whose new thread id byre then stores, so the new conversation is the source's up to the cut, under its own id, and later starts resume the new id as usual. A forked Codex thread keeps the source's byre tools. Refused (409) while the source task is closed (its snapshot refs are gone), when there is no cut point, and when the snapshot's session runs on Antigravity, which can't fork a conversation; 404 for an unknown snapshot. -The cut point comes from `vendor_uuid`: every top-level `agent.message`, `agent.thinking`, `agent.tool_call` and `agent.tool_result` event (not a subagent's) carries the `uuid` of the Claude Code stream-json line it came from, which is the id of that entry in the CLI's own session transcript. `forkPoint` takes the last such event of the snapshot's session at or before the snapshot's `event_seq`, then extends the cut through the following tool calls and results until every tool call it has kept has its result, so the fork never resumes on an unanswered tool call. For a `turn_end` snapshot the cut is the turn's last message. A Codex session's events carry no `vendor_uuid` except its `turn.ended`, which carries the turn id, so for Codex `forkPoint` picks the last turn that ended at or before the snapshot. Snapshots whose session recorded no `vendor_uuid` by then, such as those from before byre recorded it, can't be forked. +The cut point comes from `vendor_uuid`: every top-level `agent.message`, `agent.thinking`, `agent.tool_call` and `agent.tool_result` event (not a subagent's) carries the `uuid` of the Claude Code stream-json line it came from, which is the id of that entry in the CLI's own session transcript. `forkPoint` takes the last such event of the snapshot's session at or before the snapshot's `event_seq`, then extends the cut through the following tool calls and results until every tool call it has kept has its result, so the fork never resumes on an unanswered tool call. When the events run out with calls still open, as when the agent forks from inside its own `fork` call, the cut goes back to just before the message holding the first of them (every block of one Claude message carries the same `vendor_uuid`). For a `turn_end` snapshot the cut is the turn's last message. A Codex session's events carry no `vendor_uuid` except its `turn.ended`, which carries the turn id, so for Codex `forkPoint` picks the last turn that ended at or before the snapshot. Snapshots whose session recorded no `vendor_uuid` by then, such as those from before byre recorded it, can't be forked. A fork of a workflow stage task gets a copy of the source's frozen workflow, brief, run and stage, not the library's current workflow, so the fork is another attempt at that stage of the same run in its own worktree; when it finishes, the stages after it start from it unless they already have tasks (see "Workflows"). @@ -447,6 +451,8 @@ The fork's session guidance gets one more line (`forkGuidance` in `internal/agen An answered question can be answered again. Expanding an answered question's bubble in the transcript shows an **Answer again** button when the question is `answered` (the transcript's copy of the question comes from its `inbox.*` events, which carry no `snapshot_n`, so the server is what checks for one); it reopens the card's choices, Other box and "Let the agent decide", with Cancel in place of Ignore, and sending posts the new answer, an `InboxResponseCreate`, to `POST /api/inbox/answer-again` (`AnswerAgain` in `internal/agents/fork.go`) instead of `respondInbox`. byre checks the answer against the question as if it were open, forks the question's task at the answer's `snapshot_n` exactly as the **fork** button does, and files in the fork a copy of the question (same payload, its own id and thread) already `answered` with the new response, in one transaction (`Store.CreateAnsweredItem`), whose `inbox.created` event carries the answered item and the human as actor. `Deliver` then hands that answer to the fork's idle session as its first turn, the usual delivery message, so the forked conversation, cut just before the original answer reached the agent, continues with the new one. The app opens the fork; the original task and its question are untouched. An `inbox.created` for an item that isn't open raises no notification, and the lead's signal for it reads as an answer, not a question. Refused (409) when the question isn't `answered` or its answer has no `snapshot_n`, and for anything the fork refuses; 400 for an answer that doesn't fit the question. +An implementer on Claude Code forks its own task with the `fork` MCP tool (`forkTool` in `internal/agents/fork.go`), `title` and `prompt`, for when two ways forward are both worth finishing; its description asks the agent to fork only when it strongly believes both are worth following. byre snapshots the worktree there and then (trigger `fork`, synchronously under the task's mutex like an `answer` snapshot, and like one never reviewed), forks the task at that snapshot as the **fork** button does but titled `title`, as the session's creator, and sends `prompt` to the fork's idle session as its first message, actor `agent`. Codex and Antigravity sessions don't get it, since neither can cut a conversation mid-turn. The fork's conversation is cut just before the `fork` call, so its agent never sees the call and takes the prompt's path; the calling agent gets the fork's id, branch and worktree, and carries on with its own. + ## Reviews Every new snapshot of a task gets a review: a separate agent session on the vendor the task's agent doesn't use reads the change and reports on it (`internal/agents/review.go`, `internal/store/review.go`). A turn that changes no files makes no snapshot and so no review, an interrupted turn makes no snapshot (see "Snapshots"), and a snapshot whose change (the diff below) has no files runs no reviewer: it gets a `reviews` row in state `skipped`, with no session, vendor or model. The snapshot at the end of a Merge turn that finishes into the squash (the turn ended cleanly with `merging_by` still set, read by `turnEnded` before it asks for the snapshot) gets no review and no `reviews` row at all, since the task closes a moment later; a Merge turn that errors or calls `stop_merge` is reviewed as usual. Besides `take_snapshot` (see "Snapshots"), nothing else starts a review, and nothing waits for one: reviews ride along for the human to look at when they want. diff --git a/internal/agents/afterreview.go b/internal/agents/afterreview.go index 2823a29..8084bb9 100644 --- a/internal/agents/afterreview.go +++ b/internal/agents/afterreview.go @@ -34,7 +34,7 @@ func (m *Manager) MergeAfterReview(ctx context.Context, task api.Task, user api. } } - latest, err := m.store.LastSnapshotNot(ctx, task.Id, api.SnapshotTriggerAnswer) + latest, err := m.store.LastReviewable(ctx, task.Id) if errors.Is(err, store.ErrNotFound) { return api.Task{}, fmt.Errorf("%w: task %s has no snapshot under review", ErrConflict, task.Id) } @@ -146,7 +146,7 @@ func (m *Manager) startMergeAfterReview(ctx context.Context, r api.Review, mar s if task.Discarding { return "the task is being discarded" } - latest, err := m.store.LastSnapshotNot(ctx, task.Id, api.SnapshotTriggerAnswer) + latest, err := m.store.LastReviewable(ctx, task.Id) if err != nil { return err.Error() } diff --git a/internal/agents/away.go b/internal/agents/away.go new file mode 100644 index 0000000..6ca463b --- /dev/null +++ b/internal/agents/away.go @@ -0,0 +1,49 @@ +package agents + +import ( + "context" + "log/slog" + + "g.fc.run/ai/byre/internal/api" + "g.fc.run/ai/byre/internal/store" +) + +const awayText = "I'm away right now, so I can't answer this. Pick the answer you think most likely and carry on; when I'm back I can answer again, which forks the task from here with my answer. " + + "If you strongly believe two answers are both worth following to the end, use your fork tool, if you have one, to follow the other one in a fork. " + + "If this needs something only I can give, such as a test result, a password or a permission, don't guess: note it, and carry on with other work or end your turn. " + + "This grants no authorization I must give explicitly, such as to deploy, push or commit." + +func (m *Manager) SetAway(ctx context.Context, login string, away bool) (api.User, error) { + u, err := m.store.UpdateAway(ctx, login, away) + if err != nil || !away { + return u, err + } + items, err := m.store.ListInbox(ctx, store.InboxFilter{State: api.Open}) + if err != nil { + return u, err + } + for _, item := range items { + task, err := m.store.GetTask(ctx, item.TaskId) + if err != nil { + return u, err + } + if task.CreatedBy != login { + continue + } + err = m.answerAway(ctx, login, item.Id) + if err != nil { + slog.Error("answer question while away", "item", item.Id, "user", login, "err", err) + } + } + return u, nil +} + +func (m *Manager) answerAway(ctx context.Context, login, itemID string) error { + text := awayText + _, err := m.Respond(ctx, login, []api.InboxResponseCreate{{ItemId: itemID, Type: api.Other, Text: &text}}) + if err != nil { + return err + } + slog.Info("question answered while away", "item", itemID, "user", login) + return nil +} diff --git a/internal/agents/fork.go b/internal/agents/fork.go index b69a4f1..b51b789 100644 --- a/internal/agents/fork.go +++ b/internal/agents/fork.go @@ -2,19 +2,88 @@ package agents import ( "context" + "encoding/json" + "errors" "fmt" "log/slog" + "strings" "time" "g.fc.run/ai/byre/internal/api" + "g.fc.run/ai/byre/internal/mcp" "g.fc.run/ai/byre/internal/store" ) func (m *Manager) Fork(ctx context.Context, src api.Task, project api.Project, n int, creator api.User) (api.Task, error) { - t, _, err := m.fork(ctx, src, project, n, creator) + t, _, err := m.fork(ctx, src, project, n, creator, forkTitle(src, n)) return t, err } +func forkTitle(src api.Task, n int) string { + return fmt.Sprintf("%s (fork of #%d)", src.Title, n) +} + +func (m *Manager) forkTool(l *live) mcp.Tool { + return mcp.Tool{ + Name: "fork", + Description: "Fork this task here, for when two ways forward are both worth finishing and the human should compare the results rather than you picking one. " + + "byre snapshots your worktree now and starts a new task from that snapshot, with its own branch and worktree, whose agent continues this conversation up to just before this call " + + "and then gets prompt as its next message. You carry on here with the other way. " + + "Each fork doubles the work and gives the human another result to compare, so fork only when you strongly believe both paths are worth following.", + InputSchema: json.RawMessage(`{"type": "object", "properties": { + "title": {"type": "string", "description": "The new task's title, naming the path it takes."}, + "prompt": {"type": "string", "description": "The forked agent's next message: tell it that it is the other side of a fork and which path to take."} + }, "required": ["title", "prompt"]}`), + Handler: func(ctx context.Context, raw json.RawMessage) (string, error) { + args := struct { + Title string `json:"title"` + Prompt string `json:"prompt"` + }{} + err := json.Unmarshal(raw, &args) + if err != nil { + return "", fmt.Errorf("malformed fork call: %w", err) + } + if strings.TrimSpace(args.Title) == "" || strings.TrimSpace(args.Prompt) == "" { + return "", errors.New("title and prompt are required") + } + sess, err := m.store.GetSession(ctx, l.id) + if err != nil { + return "", err + } + creator, err := m.store.GetUser(ctx, sess.CreatedBy) + if err != nil { + return "", err + } + src, err := m.store.GetTask(ctx, l.taskID) + if err != nil { + return "", err + } + project, err := m.store.GetProject(ctx, src.ProjectId) + if err != nil { + return "", err + } + snap, err := m.cutSnapshot(ctx, liveTarget(l), m.ownsTask(l.id), api.SnapshotTriggerFork) + if err != nil { + return "", err + } + if snap == nil { + return "", fmt.Errorf("%w: task %s is closing", ErrConflict, src.Id) + } + t, sessionID, err := m.fork(ctx, src, project, snap.N, creator, args.Title) + if err != nil { + return "", err + } + _, _, err = m.Message(ctx, sessionID, agentActor, args.Prompt, "") + if err != nil { + return "", fmt.Errorf("task %s forked, but its agent did not start: %w", t.Id, err) + } + slog.Info("task forked by its agent", "task", src.Id, "fork", t.Id, "snapshot", snap.N) + return fmt.Sprintf("Forked at snapshot #%d as task %s %q, branch %s, worktree %s; its agent continues from just before this call with your prompt. Carry on here with your own path.", + snap.N, t.Id, t.Title, t.Branch, t.WorktreePath), nil + }, + } +} + func (m *Manager) AnswerAgain(ctx context.Context, creator api.User, create api.InboxResponseCreate) (api.Task, error) { item, err := m.store.GetInboxItem(ctx, create.ItemId) if err != nil { @@ -47,7 +116,7 @@ func (m *Manager) AnswerAgain(ctx context.Context, creator api.User, create api. if err != nil { return api.Task{}, err } - t, sessionID, err := m.fork(ctx, src, project, n, creator) + t, sessionID, err := m.fork(ctx, src, project, n, creator, forkTitle(src, n)) if err != nil { return api.Task{}, err } @@ -76,7 +145,7 @@ func (m *Manager) AnswerAgain(ctx context.Context, creator api.User, create api. return t, nil } -func (m *Manager) fork(ctx context.Context, src api.Task, project api.Project, n int, creator api.User) (api.Task, string, error) { +func (m *Manager) fork(ctx context.Context, src api.Task, project api.Project, n int, creator api.User, title string) (api.Task, string, error) { if creator.GitName == "" || creator.GitEmail == "" { return api.Task{}, "", ErrNoGitIdentity } @@ -114,7 +183,7 @@ func (m *Manager) fork(ctx context.Context, src api.Task, project api.Project, n } t, err := m.createTask(ctx, project, api.Task{ - Title: fmt.Sprintf("%s (fork of #%d)", src.Title, n), + Title: title, MergeTitle: src.MergeTitle, CreatedBy: creator.Login, TodoItem: src.TodoItem, @@ -150,6 +219,8 @@ func (m *Manager) fork(ctx context.Context, src api.Task, project api.Project, n func forkPoint(events []api.Event, sessionID string, seq int64) string { at := "" + prev := "" + cut := "" open := map[string]bool{} for _, ev := range events { if ev.AgentSessionId == nil || *ev.AgentSessionId != sessionID { @@ -168,18 +239,31 @@ func forkPoint(events []api.Event, sessionID string, seq int64) string { } after := ev.Seq > seq if after && (len(open) == 0 || id == "") { - break + return at } switch ev.Kind { case "agent.tool_call": + if len(open) == 0 { + cut = at + if at == uuid { + cut = prev + } + } open[id] = true case "agent.tool_result": delete(open, id) } + if uuid != at { + prev = at + } at = uuid if after && len(open) == 0 { - break + return at } } + // Calls still open have no results yet, as when the agent forks from inside a tool call: cut before them. + if len(open) > 0 { + return cut + } return at } diff --git a/internal/agents/inbox.go b/internal/agents/inbox.go index d970f29..46ad558 100644 --- a/internal/agents/inbox.go +++ b/internal/agents/inbox.go @@ -146,6 +146,22 @@ func (m *Manager) ask(ctx context.Context, l *live, raw json.RawMessage) (string l.asked = true m.mu.Unlock() + task, err := m.store.GetTask(ctx, l.taskID) + if err != nil { + return "", err + } + creator, err := m.store.GetUser(ctx, task.CreatedBy) + if err != nil { + return "", err + } + if creator.Away { + err = m.answerAway(ctx, creator.Login, id) + if err == nil { + return fmt.Sprintf("Question %s filed, but %s is away, so byre answered it for them at once; the answer reaches you next.", id, creator.Login), nil + } + slog.Error("answer question while away", "item", id, "user", creator.Login, "err", err) + } + return fmt.Sprintf("Question filed in the human's inbox. question_id: %s. End your turn now. The answer will arrive as your next message. "+ "If the human answers it some other way, such as in a chat message, withdraw it with cancel_question and question_id %q.", id, id), nil } diff --git a/internal/agents/manager.go b/internal/agents/manager.go index 4128a2d..ab3e7d4 100644 --- a/internal/agents/manager.go +++ b/internal/agents/manager.go @@ -752,6 +752,9 @@ func (m *Manager) startLive(ctx context.Context, sess api.AgentSession, task api } l.notes = "\n\n" + taskNotesGuidance + "\n\n" + notes tools = append(tools, m.suggestNoteTool(l), m.readyForMergeTool(l), m.suggestDiscardTool(l), m.respondToFindingTool(l)) + if sess.Vendor == api.Claude { + tools = append(tools, m.forkTool(l)) + } } if sess.Role == api.Implementer && sess.Member != "" { sessions, err := m.store.ListSessions(ctx, task.Id) diff --git a/internal/agents/ready.go b/internal/agents/ready.go index e838eac..fcd5217 100644 --- a/internal/agents/ready.go +++ b/internal/agents/ready.go @@ -130,7 +130,7 @@ func (m *Manager) tryPromptReady(ctx context.Context, taskID string, fromReview if err != nil || sess.State == api.AgentSessionStateRunning || m.interrupted(sess.Id) { return err } - latest, err := m.store.LastSnapshotNot(ctx, taskID, api.SnapshotTriggerAnswer) + latest, err := m.store.LastReviewable(ctx, taskID) if errors.Is(err, store.ErrNotFound) { return nil } diff --git a/internal/agents/snapshots.go b/internal/agents/snapshots.go index deb1c39..72c83f3 100644 --- a/internal/agents/snapshots.go +++ b/internal/agents/snapshots.go @@ -145,7 +145,7 @@ func (m *Manager) takeSnapshot(ctx context.Context, t snapTarget, trigger api.Sn if err != nil && !errors.Is(err, store.ErrNotFound) { return nil, err } - reviewed, err := m.store.LastSnapshotNot(ctx, t.taskID, api.SnapshotTriggerAnswer) + reviewed, err := m.store.LastReviewable(ctx, t.taskID) if err != nil && !errors.Is(err, store.ErrNotFound) { return nil, err } @@ -229,6 +229,19 @@ func (m *Manager) sessionTarget(ctx context.Context, sess api.AgentSession, task } func (m *Manager) snapshotForAnswer(ctx context.Context, t snapTarget, held bool) int { + snap, err := m.cutSnapshot(ctx, t, held, api.SnapshotTriggerAnswer) + if err != nil { + slog.Error("take snapshot", "task", t.taskID, "trigger", api.SnapshotTriggerAnswer, "err", err) + m.recordError(&live{id: t.sessionID, taskID: t.taskID, projectID: t.projectID}, fmt.Sprintf("byre couldn't snapshot the worktree before handing over an answer, so the answer went to the agent without one: %v", err)) + return 0 + } + if snap == nil { + return 0 + } + return snap.N +} + +func (m *Manager) cutSnapshot(ctx context.Context, t snapTarget, held bool, trigger api.SnapshotTrigger) (*api.Snapshot, error) { if !held { unlock := m.lockTask(t.taskID) defer unlock() @@ -242,16 +255,9 @@ func (m *Manager) snapshotForAnswer(ctx context.Context, t snapTarget, held bool closed := st.closed m.snapMu.Unlock() if closed { - return 0 + return nil, nil } - - snap, err := m.takeSnapshot(ctx, t, api.SnapshotTriggerAnswer, 0, true) - if err != nil { - slog.Error("take snapshot", "task", t.taskID, "trigger", api.SnapshotTriggerAnswer, "err", err) - m.recordError(&live{id: t.sessionID, taskID: t.taskID, projectID: t.projectID}, fmt.Sprintf("byre couldn't snapshot the worktree before handing over an answer, so the answer went to the agent without one: %v", err)) - return 0 - } - return snap.N + return m.takeSnapshot(ctx, t, trigger, 0, true) } func (m *Manager) takeSnapshotTool(l *live) mcp.Tool { diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index bf47bee..13896ee 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -346,6 +346,7 @@ func (e ReviewState) Valid() bool { const ( SnapshotTriggerAnswer SnapshotTrigger = "answer" SnapshotTriggerAskHuman SnapshotTrigger = "ask_human" + SnapshotTriggerFork SnapshotTrigger = "fork" SnapshotTriggerMessage SnapshotTrigger = "message" SnapshotTriggerRequested SnapshotTrigger = "requested" SnapshotTriggerTimer SnapshotTrigger = "timer" @@ -360,6 +361,8 @@ func (e SnapshotTrigger) Valid() bool { return true case SnapshotTriggerAskHuman: return true + case SnapshotTriggerFork: + return true case SnapshotTriggerMessage: return true case SnapshotTriggerRequested: @@ -695,6 +698,11 @@ type Attachment struct { // AttachmentKind defines model for Attachment.Kind. type AttachmentKind string +// Away defines model for Away. +type Away struct { + Away bool `json:"away"` +} + // Blob defines model for Blob. type Blob struct { ContentType string `json:"content_type"` @@ -1178,11 +1186,11 @@ type Snapshot struct { TaskId string `json:"task_id"` TreeSha string `json:"tree_sha"` - // Trigger turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. answer: just before answers from the human were handed to the agent. message (the agent wrote text after one or more tool calls), tool_call, ask_human and timer appear only on snapshots taken by earlier builds. + // Trigger turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. answer: just before answers from the human were handed to the agent. fork: when the task's agent forked the task with its fork tool. answer and fork snapshots are never reviewed. message (the agent wrote text after one or more tool calls), tool_call, ask_human and timer appear only on snapshots taken by earlier builds. Trigger SnapshotTrigger `json:"trigger"` } -// SnapshotTrigger turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. answer: just before answers from the human were handed to the agent. message (the agent wrote text after one or more tool calls), tool_call, ask_human and timer appear only on snapshots taken by earlier builds. +// SnapshotTrigger turn_end: after byre's end-of-turn commit. requested: a task's agent or the lead asked for it with take_snapshot. answer: just before answers from the human were handed to the agent. fork: when the task's agent forked the task with its fork tool. answer and fork snapshots are never reviewed. message (the agent wrote text after one or more tool calls), tool_call, ask_human and timer appear only on snapshots taken by earlier builds. type SnapshotTrigger string // Stage defines model for Stage. @@ -1507,6 +1515,9 @@ type UsageWindow struct { // User defines model for User. type User struct { + // Away While true, byre answers the questions agents ask in this user's tasks at once. + Away bool `json:"away"` + // DefaultModel The model the app starts new tasks on. DefaultModel string `json:"default_model"` DisplayName string `json:"display_name"` @@ -1687,6 +1698,9 @@ type RespondInboxJSONRequestBody = InboxRespond // UpdateMeJSONRequestBody defines body for UpdateMe for application/json ContentType. type UpdateMeJSONRequestBody = UserUpdate +// UpdateAwayJSONRequestBody defines body for UpdateAway for application/json ContentType. +type UpdateAwayJSONRequestBody = Away + // UpdateDefaultModelJSONRequestBody defines body for UpdateDefaultModel for application/json ContentType. type UpdateDefaultModelJSONRequestBody = DefaultModel @@ -1789,6 +1803,9 @@ type ServerInterface interface { // (PUT /api/me) UpdateMe(w http.ResponseWriter, r *http.Request) + // (PUT /api/me/away) + UpdateAway(w http.ResponseWriter, r *http.Request) + // (PUT /api/me/default-model) UpdateDefaultModel(w http.ResponseWriter, r *http.Request) @@ -2367,6 +2384,20 @@ func (siw *ServerInterfaceWrapper) UpdateMe(w http.ResponseWriter, r *http.Reque handler.ServeHTTP(w, r) } +// UpdateAway operation middleware +func (siw *ServerInterfaceWrapper) UpdateAway(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.UpdateAway(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // UpdateDefaultModel operation middleware func (siw *ServerInterfaceWrapper) UpdateDefaultModel(w http.ResponseWriter, r *http.Request) { @@ -3963,6 +3994,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/me", wrapper.UpdateMe) m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/me/notifications", wrapper.UpdateNotificationPrefs) m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/me/default-model", wrapper.UpdateDefaultModel) + m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/me/away", wrapper.UpdateAway) m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/me/review-models", wrapper.UpdateReviewModels) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/me/lead/sessions", wrapper.ListLeadSessions) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/me/lead/events", wrapper.ListLeadEvents) @@ -4038,282 +4070,285 @@ 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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", - "+XQ5rXdoxt5SzYP7/F7Vet6//58DAA==", + "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", + "iPLpclpv0YydpZoH9/m9qvW8f/9/BgA=", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/server/notifications.go b/internal/server/notifications.go index a9e620b..4e05cf1 100644 --- a/internal/server/notifications.go +++ b/internal/server/notifications.go @@ -41,6 +41,20 @@ func (s *Server) UpdateDefaultModel(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, u) } +func (s *Server) UpdateAway(w http.ResponseWriter, r *http.Request) { + req := api.Away{} + if !readJSON(w, r, &req) { + return + } + + u, err := s.agents.SetAway(r.Context(), actor(r), req.Away) + if err != nil { + writeAgentError(w, err) + return + } + writeJSON(w, http.StatusOK, u) +} + func (s *Server) UpdateReviewModels(w http.ResponseWriter, r *http.Request) { req := api.ReviewModels{} if !readJSON(w, r, &req) { diff --git a/internal/store/migrations/065-add-user-away.sql b/internal/store/migrations/065-add-user-away.sql new file mode 100644 index 0000000..c3f4def --- /dev/null +++ b/internal/store/migrations/065-add-user-away.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN away INTEGER NOT NULL DEFAULT 0; diff --git a/internal/store/notifications.go b/internal/store/notifications.go index a0a6be5..8ba553b 100644 --- a/internal/store/notifications.go +++ b/internal/store/notifications.go @@ -40,6 +40,20 @@ func (s *Store) UpdateDefaultModel(ctx context.Context, login, model string) (ap return s.GetUser(ctx, login) } +func (s *Store) UpdateAway(ctx context.Context, login string, away bool) (api.User, error) { + err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { + _, err := tx.ExecContext(ctx, `UPDATE users SET away = ? WHERE login = ?`, away, login) + if err != nil { + return err + } + return emit(api.Event{Kind: "user.updated", Actor: login, Payload: map[string]any{"login": login}}) + }) + if err != nil { + return api.User{}, err + } + return s.GetUser(ctx, login) +} + func (s *Store) UpdateReviewModels(ctx context.Context, login, model, second string) (api.User, error) { err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { _, err := tx.ExecContext(ctx, `UPDATE users SET review_model = ?, second_review_model = ? WHERE login = ?`, model, second, login) diff --git a/internal/store/snapshot.go b/internal/store/snapshot.go index 0df4cd8..ed20e77 100644 --- a/internal/store/snapshot.go +++ b/internal/store/snapshot.go @@ -17,7 +17,7 @@ func (s *Store) CreateSnapshot(ctx context.Context, snap api.Snapshot, projectID if err != nil { return err } - if snap.Trigger != api.SnapshotTriggerAnswer { + if !CutPoint(snap.Trigger) { _, err = tx.ExecContext(ctx, `UPDATE tasks SET ready = '' WHERE id = ?`, snap.TaskId) if err != nil { return err @@ -68,8 +68,13 @@ func (s *Store) LastSnapshot(ctx context.Context, taskID string) (api.Snapshot, return snap, err } -func (s *Store) LastSnapshotNot(ctx context.Context, taskID string, trigger api.SnapshotTrigger) (api.Snapshot, error) { - snap, err := scanSnapshot(s.db.QueryRowContext(ctx, `SELECT `+snapshotColumns+` FROM snapshots WHERE task_id = ? AND trigger != ? ORDER BY n DESC LIMIT 1`, taskID, string(trigger))) +func CutPoint(trigger api.SnapshotTrigger) bool { + return trigger == api.SnapshotTriggerAnswer || trigger == api.SnapshotTriggerFork +} + +func (s *Store) LastReviewable(ctx context.Context, taskID string) (api.Snapshot, error) { + snap, err := scanSnapshot(s.db.QueryRowContext(ctx, `SELECT `+snapshotColumns+` FROM snapshots WHERE task_id = ? AND trigger NOT IN (?, ?) ORDER BY n DESC LIMIT 1`, + taskID, string(api.SnapshotTriggerAnswer), string(api.SnapshotTriggerFork))) if errors.Is(err, sql.ErrNoRows) { return snap, ErrNotFound } diff --git a/internal/store/store.go b/internal/store/store.go index eaee689..7cc8ce3 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -218,8 +218,8 @@ func (s *Store) EnsureUser(ctx context.Context, login, displayName string) (api. func (s *Store) GetUser(ctx context.Context, login string) (api.User, error) { u := api.User{} prefs := "" - err := s.db.QueryRowContext(ctx, `SELECT login, display_name, git_name, git_email, notification_prefs, default_model, review_model, second_review_model FROM users WHERE login = ?`, login). - Scan(&u.Login, &u.DisplayName, &u.GitName, &u.GitEmail, &prefs, &u.DefaultModel, &u.ReviewModel, &u.SecondReviewModel) + err := s.db.QueryRowContext(ctx, `SELECT login, display_name, git_name, git_email, notification_prefs, default_model, review_model, second_review_model, away FROM users WHERE login = ?`, login). + Scan(&u.Login, &u.DisplayName, &u.GitName, &u.GitEmail, &prefs, &u.DefaultModel, &u.ReviewModel, &u.SecondReviewModel, &u.Away) if errors.Is(err, sql.ErrNoRows) { return u, ErrNotFound } @@ -751,9 +751,9 @@ func (s *Store) ReadyPromptDue(ctx context.Context, id string) (bool, error) { err := s.db.QueryRowContext(ctx, `SELECT EXISTS ( SELECT 1 FROM events e, tasks t WHERE t.id = ? AND e.task_id = t.id AND e.seq > t.ready_prompt_seq - AND ((e.kind = 'snapshot.created' AND json_extract(e.payload, '$.trigger') != ?) + AND ((e.kind = 'snapshot.created' AND json_extract(e.payload, '$.trigger') NOT IN (?, ?)) OR (e.kind = 'user.message' AND e.actor NOT IN ('byre', ?) AND e.actor NOT LIKE '% (lead)')) - )`, id, string(api.SnapshotTriggerAnswer), agentActor).Scan(&due) + )`, id, string(api.SnapshotTriggerAnswer), string(api.SnapshotTriggerFork), agentActor).Scan(&due) return due, err } diff --git a/mac/Sources/Byre/ByreApp.swift b/mac/Sources/Byre/ByreApp.swift index b42eebc..a963524 100644 --- a/mac/Sources/Byre/ByreApp.swift +++ b/mac/Sources/Byre/ByreApp.swift @@ -68,6 +68,7 @@ struct ByreApp: App { ServerWindow() } .defaultSize(width: 1280, height: 820) + .windowToolbarStyle(.unifiedCompact) .restorationBehavior(.disabled) .defaultLaunchBehavior(.presented) .commands { diff --git a/mac/Sources/Byre/Chat.swift b/mac/Sources/Byre/Chat.swift index 2ae27d6..e1f9622 100644 --- a/mac/Sources/Byre/Chat.swift +++ b/mac/Sources/Byre/Chat.swift @@ -1619,7 +1619,7 @@ private struct SnapshotRow: View { .buttonStyle(SmallLinkButtonStyle()) .disabled(forking) .help("Start a new task from this snapshot, its agent continuing the conversation as it stood here") - if snap.trigger != .answer { + if ![.answer, .fork].contains(snap.trigger) { reviewButton } Rectangle().fill(Palette.border).frame(height: 1) diff --git a/mac/Sources/Byre/MergeDialog.swift b/mac/Sources/Byre/MergeDialog.swift index 0657a05..337ccae 100644 --- a/mac/Sources/Byre/MergeDialog.swift +++ b/mac/Sources/Byre/MergeDialog.swift @@ -160,7 +160,7 @@ private func reviewWarnings(_ taskId: String, _ store: Store, _ leave: @escaping async let snapsCall = client.listSnapshots(path: .init(taskId: taskId)).ok.body.json async let reviewsCall = client.listReviews(path: .init(taskId: taskId)).ok.body.json let (snaps, reviews) = try await (snapsCall, reviewsCall) - guard let latest = snaps.last(where: { $0.trigger != .answer }) else { + guard let latest = snaps.last(where: { ![.answer, .fork].contains($0.trigger) }) else { return ([MergeWarning(text: "These changes haven't been reviewed")], false) } var items: [MergeWarning] = [] diff --git a/mac/Sources/Byre/Startup.swift b/mac/Sources/Byre/Startup.swift index 400e0c3..eddea7f 100644 --- a/mac/Sources/Byre/Startup.swift +++ b/mac/Sources/Byre/Startup.swift @@ -494,12 +494,56 @@ struct ServerPicker: View { .padding(.horizontal, 8) .padding(.vertical, 3) .help("Add Server") + AwayToggle() } .font(Fonts.font(13)) .fixedSize() } } +private struct AwayToggle: View { + @ViewState private var hover = false + @ViewState private var busy = false + + var body: some View { + let stores = Servers.shared.origins.compactMap { Servers.shared.connections[$0]?.store } + let away = stores.contains { $0.me?.away == true } + if !stores.isEmpty { + Button { + set(!away, stores) + } label: { + Text("Away") + .foregroundStyle(away ? Palette.accent : hover ? Palette.text : Palette.muted) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .background(away ? Palette.accentTint : .clear, in: RoundedRectangle(cornerRadius: 6)) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .disabled(busy) + .padding(.leading, 8) + .help(away + ? "You're away: byre answers your agents' questions at once, telling them to pick the most likely answer. Click when you're back." + : "Go away: byre answers your agents' questions at once, telling them to pick the most likely answer, on every server") + .onHover { hover = $0 } + } + } + + private func set(_ away: Bool, _ stores: [Store]) { + busy = true + Task { + for store in stores { + do { + store.me = try await store.api.client.updateAway(body: .json(.init(away: away))).ok.body.json + } catch { + log.error("set away: \(errorText(error), privacy: .public)") + } + } + busy = false + } + } +} + private struct ServerChip: View { let origin: String @ViewState private var hover = false diff --git a/mac/Sources/Byre/Store.swift b/mac/Sources/Byre/Store.swift index 1cd0ca4..f91069d 100644 --- a/mac/Sources/Byre/Store.swift +++ b/mac/Sources/Byre/Store.swift @@ -624,7 +624,7 @@ final class Store { try await self.loadProjects() } case "task.created", "task.merge_title", "task.merge_queued", "task.merge_unqueued", "task.merge_after_review", "task.merge_after_review_ended", "task.merge_after_review_cancelled", "task.parent_merge_started", "task.parent_merge_stopped", "task.merge_started", "task.merge_ended", "task.discard_started", "task.merged", "task.discarded", "task.state", "task.ready", "task.discard_suggested", "snapshot.created": - if let taskId = ev.taskId, ["task.merged", "task.discarded"].contains(ev.kind) || (ev.kind == "snapshot.created" && str(ev.payload.value, "trigger") != "answer") { + if let taskId = ev.taskId, ["task.merged", "task.discarded"].contains(ev.kind) || (ev.kind == "snapshot.created" && !["answer", "fork"].contains(str(ev.payload.value, "trigger"))) { clearTask(self, taskId) } notifyTask(self, ev) diff --git a/mac/Sources/Byre/TranscriptBuilder.swift b/mac/Sources/Byre/TranscriptBuilder.swift index f8de1fd..79b5bce 100644 --- a/mac/Sources/Byre/TranscriptBuilder.swift +++ b/mac/Sources/Byre/TranscriptBuilder.swift @@ -467,7 +467,7 @@ final class TranscriptBuilder { let snap = try! decodePayload(ev.payload, as: Snapshot.self) let row = Row(.snapshot(snap), taskId: taskId) ctx.reviewButtons[snap.n] = row - if snap.trigger != .answer && sink == nil { + if ![.answer, .fork].contains(snap.trigger) && sink == nil { lastReady = nil } put(row)