|
|
|
@@ -273,7 +273,7 @@ Agent health is `internal/agents/health.go`:
|
|
|
|
|
|
|
|
|
|
The left pane's **Status** tab shows, from the top: a Problems section only when there are zombies or protocol errors (one line per zombie, and per vendor the error count with the latest line folded under it); each running turn, linking to its task, with the time since its process's last event and `stuck` or `exited` when it is; each vendor's usage, headed `rejected` (with the `rejected` reason when there is one) when `rejected` is set or any window's `used` has reached its `limit` (else `allowed`) and the time since its `updated_at`, and per window its label, the amount used formatted by `unit` (`$150.94 / $200.00` for `usd`, `36 / 40 requests`, `1.2M tokens` without a limit, empty for `percent`), the share used in its own right-aligned column (`75%`; `used` itself for `percent`, `—` without a limit), the share of the window's time elapsed (from `resets_at` and `window_seconds`, `—` without both), a pace bar whose fill is the share used and whose vertical line is the share elapsed, the fill warn-coloured when usage is ahead of the line, all vendors in one table so their columns and pace bars line up, each cell on one line and the table scrolling sideways when it is wider than the pane; a Clients table, one row per entry in `clients`, with the Mac, user, the build's commit (cut to 8 characters, the full one in its tooltip), the time since it connected and the time since its last action; and the server counts. It refetches the status when a `session.created`, `session.state`, `task.*`, `project.created`, `admin.health` or `admin.clients` event arrives, when an `agent.vendor` event carries a `rate_limit_event`, and when the SSE stream reconnects. Other events advance the last-event seq it shows, and any event carrying a process's `agent_session_id` (stored or not, such as `agent.delta`) moves that process's last-event time forward, re-rendering at most once a second per process. A re-render keeps the scroll position and which folded lines are open. There is no timer, so "time since last event" and the "in 2h 10m" beside a reset time are as of the last render; a process going quiet long enough to be stuck shows up when the next health check broadcasts `admin.health`.
|
|
|
|
|
|
|
|
|
|
Agent activity becomes events with `agent_session_id` set: `user.message`, `agent.message`, `agent.thinking`, `agent.tool_call`, `agent.tool_result`, `turn.ended`, `session.state`, `session.subagents`, `session.model`, and `session.context`. Anything else Claude Code emits (rate-limit status, task progress, state changes) is recorded unchanged as `agent.vendor`. Codex items map onto the same kinds (`handleCodex` in `internal/agents/codex.go`): an `agentMessage` becomes `agent.message`, a `reasoning` item's summary `agent.thinking`, and `commandExecution` (as `Bash`), `fileChange` (as `apply_patch`), `dynamicToolCall` (as `mcp__<namespace>__<tool>`, so byre's tools carry the same names as Claude's), `mcpToolCall` and `webSearch` items an `agent.tool_call` when they start and an `agent.tool_result` when they complete. Other completed items and the `error` and `warning` notifications are recorded as `agent.vendor`; the remaining notifications (thread status, MCP startup and the like) aren't recorded. `item/agentMessage/delta` becomes `agent.delta`, and `thread/tokenUsage/updated` `session.context`. Antigravity steps map the same way (`handleAntigravity` in `internal/agents/antigravity.go`): an `agent_response` step's `text_delta`s become `agent.delta` and its finished text `agent.message`, and a `tool` step an `agent.tool_call` when it starts and an `agent.tool_result` when it is done, with `run_command` recorded as `Bash` and `call_mcp_tool` as `mcp__<server>__<tool>` with its `Arguments` as input; `user_input` steps aren't recorded, and other step types are recorded as `agent.vendor`. The one exception is partial output: each top-level `text_delta` is broadcast to SSE clients as an `agent.delta` event (`payload.text`, `seq` 0) that is never stored, and the transcript grows a live message from them until the finished `agent.message` replaces it. A `turn.ended` event carries `summary: true` when its `result` is the turn's summary (see above). Activity inside a subagent (`agent.message`, `agent.thinking`, `agent.tool_call`, `agent.tool_result`) carries `payload.parent_tool_use_id`, the ID of the tool call that started the subagent, and never updates `session.context`. `session.context` records the context size whenever it changes: `used` is the last assistant message's input, cache-read, cache-creation and output tokens, and `window` is the model's context window from the turn's `result` (`modelUsage[model].contextWindow`), so it is 0 until the session's first turn after a server start ends. The task header's **Compact** button shows the latest one as a background fill across the button, proportional to the window, tinted amber from 70% and red from 90%; the exact count is in its tooltip.
|
|
|
|
|
Agent activity becomes events with `agent_session_id` set: `user.message`, `agent.message`, `agent.thinking`, `agent.tool_call`, `agent.tool_result`, `turn.ended`, `session.state`, `session.subagents`, `session.model`, and `session.context`. Anything else Claude Code emits (rate-limit status, task progress, state changes) is recorded unchanged as `agent.vendor`. Codex items map onto the same kinds (`handleCodex` in `internal/agents/codex.go`): an `agentMessage` becomes `agent.message`, a `reasoning` item's summary `agent.thinking`, and `commandExecution` (as `Bash`), `fileChange` (as `apply_patch`), `dynamicToolCall` (as `mcp__<namespace>__<tool>`, so byre's tools carry the same names as Claude's), `mcpToolCall` and `webSearch` items an `agent.tool_call` when they start and an `agent.tool_result` when they complete. Other completed items and the `error` and `warning` notifications are recorded as `agent.vendor`; the remaining notifications (thread status, MCP startup and the like) aren't recorded. `item/agentMessage/delta` becomes `agent.delta`, and `thread/tokenUsage/updated` `session.context`. Antigravity steps map the same way (`handleAntigravity` in `internal/agents/antigravity.go`): an `agent_response` step's `text_delta`s become `agent.delta` and its finished text `agent.message`, and a `tool` step an `agent.tool_call` when it starts and an `agent.tool_result` when it is done, with `run_command` recorded as `Bash`, `read_file` and `view_file` as `Read` with their `Path` or `AbsolutePath` as `file_path`, and `call_mcp_tool` as `mcp__<server>__<tool>` with its `Arguments` as input; `user_input` steps aren't recorded, and other step types are recorded as `agent.vendor`. The one exception is partial output: each top-level `text_delta` is broadcast to SSE clients as an `agent.delta` event (`payload.text`, `seq` 0) that is never stored, and the transcript grows a live message from them until the finished `agent.message` replaces it. A `turn.ended` event carries `summary: true` when its `result` is the turn's summary (see above). Activity inside a subagent (`agent.message`, `agent.thinking`, `agent.tool_call`, `agent.tool_result`) carries `payload.parent_tool_use_id`, the ID of the tool call that started the subagent, and never updates `session.context`. `session.context` records the context size whenever it changes: `used` is the last assistant message's input, cache-read, cache-creation and output tokens, and `window` is the model's context window from the turn's `result` (`modelUsage[model].contextWindow`), so it is 0 until the session's first turn after a server start ends. The task header's **Compact** button shows the latest one as a background fill across the button, proportional to the window, tinted amber from 70% and red from 90%; the exact count is in its tooltip.
|
|
|
|
|
|
|
|
|
|
Event payloads are stored whole in `events.payload`, and `agent.tool_result` content (full command output, file reads, diffs) dominates the log's size; the app loads every active task's whole history when it starts (see "Events"), so this is what its startup costs. `sqlite3 byre.db "SELECT kind, count(*), sum(length(payload)) FROM events GROUP BY kind ORDER BY 3 DESC"` shows the split, and `sqlite3 byre.db "SELECT kind, count(*), sum(length(payload)) FROM events WHERE task_id = '<task-id>' GROUP BY kind ORDER BY 3 DESC"` one task's.
|
|
|
|
|
|
|
|
|
@@ -378,7 +378,7 @@ Answers reach the agent as one message, listing each answer under the question i
|
|
|
|
|
|
|
|
|
|
Task agents use the Macs the human has byre's app open on directly, with no approval, as they use the machine byre runs on. While it runs, the app (`mac/Sources/Byre/Remote.swift`, started with the global event stream and stopped with it) keeps a WebSocket open to `GET /api/clients/connect`, reconnecting 2s after it drops. The connection is keyed by its `Byre-Client` header and named by its `Byre-Host` header (both required); a newer connection with the same `Byre-Client` replaces the older one, while two apps open on one Mac each keep their own. The client tools take the host name, or the `id` from `list_clients`, which they need when more than one app is connected from that host (the call fails listing the ids). byre keeps the connections in memory (`internal/clients`), pinging each every 30s and dropping one that doesn't answer within 15s. The app pings too, every 10s, and reconnects when it has had no pong for 25s. That catches a socket byre dropped while the Mac slept: the close never reached the Mac, and without something sent on it the app would wait on it indefinitely. Over it byre sends `ClientRequest` text messages, each with a fresh `id`, and the app answers each with a `ClientReply` carrying that `id` (`error` when it couldn't carry it out), in any order, handling requests concurrently. A request whose client disconnects before replying fails with "<host> disconnected before replying".
|
|
|
|
|
|
|
|
|
|
Implementer sessions get ten MCP tools for it (`internal/agents/clients.go`). The app sets its WebSocket's `maximumMessageSize` to 256 MiB, the same as byre's read limit, so file contents fit in one message each way.
|
|
|
|
|
Implementer sessions get eleven MCP tools for it (`internal/agents/clients.go`). The app sets its WebSocket's `maximumMessageSize` to 256 MiB, the same as byre's read limit, so file contents fit in one message each way.
|
|
|
|
|
|
|
|
|
|
- `list_clients`: the status's `clients` (see "Status"), with each one's host, login, `id`, connection time, app commit and `last_action_at`, and its iCloud sync error when its `sync` failed; the most recently active is listed first and marked.
|
|
|
|
|
- `client_bash`: `op: bash`. The app runs `/bin/zsh -l -c <command>` in the human's home directory through `Process`, which starts it in its own process group, stdin from `/dev/null` and stdout and stderr into one temporary file, and kills the group with `SIGKILL` after `timeout_ms` (default 120000, at most 600000), replying `exit_code` -1. Otherwise `exit_code` is the exit status, or 128 plus the signal. `output` is the file's last 64 KiB. The agent's call waits the timeout plus 30s for the reply. Background children the command leaves are not waited for or killed, and a command that quits the app keeps running.
|
|
|
|
@@ -389,6 +389,7 @@ Implementer sessions get ten MCP tools for it (`internal/agents/clients.go`). Th
|
|
|
|
|
- `client_write`: `op: write_file` with `path` and `data`; on a detached task, off the main actor, the app resolves symlinks, refuses an existing path that isn't a regular file (a FIFO would block the write), creates the parent directories and writes the file in place (`Data.write` without `.atomic`), so an existing file keeps its permissions.
|
|
|
|
|
- `client_edit`: a `read_file`, then a `write_file` of the text with `old_string` replaced by `new_string`, refused unless `old_string` appears exactly once. Nothing locks the file or checks it is unchanged between the two, so a write in between, whether another agent's `client_edit` of the same file or the human's or an app's, is lost.
|
|
|
|
|
- `client_grep`: `op: bash` running `/Applications/Byre.app/Contents/Helpers/byre grep` (see "File tools") with the call's flags, pattern and `path`, each single-quoted, `~/` left outside the quotes for the shell to expand. It waits the 2 minute timeout plus 30s, and a nonzero exit fails the call with the output. A client whose app predates `byre grep` fails with its usage line.
|
|
|
|
|
- `client_list`: the same, running `byre list` with the single-quoted `path`.
|
|
|
|
|
|
|
|
|
|
`client_read`, `client_write` and `client_edit` wait at most 2 minutes for each reply, and their paths, like `client_download`'s `path`, are absolute or start with `~/`, which the app expands.
|
|
|
|
|
- `client_hands`: `op: hands`, with `tool` and `arguments` as the `hands` tools take them (see "Screen and app control"). The app runs one `byre-hands`, bundled at `Contents/Helpers/byre-hands`, shared by its connections to every server (two run from that one path would cancel each other's ScreenCaptureKit connection, see "Screen and app control"), started on first use and again after it exits, writes each call to its stdin as a `tools/call` line and replies with the line's `result`, which byre returns to the agent as the tool's result, images included (`mcp.Tool.Rich`; Codex gets them as `inputImage` data URLs). The macOS privacy grants it needs (Screen Recording, Accessibility, Automation of Orion) belong to byre's app on that Mac, since the app is the responsible process; `NSAppleEventsUsageDescription` in `mac/Info.plist` is what lets macOS ask for the Automation grant. When `byre-hands` exits, the calls waiting on it fail. The agent's call waits for the reply 2 minutes plus the call's `wait_ms`, twice its `timeout_ms` (which `browser` and `chrome` spend once on the load and again on `wait_for`) and every `act` step's `wait`, then fails.
|
|
|
|
@@ -431,10 +432,14 @@ For commands that don't need the human to see them, agents have the `bash` MCP t
|
|
|
|
|
|
|
|
|
|
### File tools
|
|
|
|
|
|
|
|
|
|
Beside `bash`, agents get `read`, `write`, `edit` and `grep` MCP tools (`internal/agents/files.go`), which byre carries out itself, as the service user and outside the agent's sandbox, the same set the client Macs get as `client_read`, `client_write`, `client_edit` and `client_grep`. A path is relative to the task's worktree, absolute, or starts with `~/` for the service user's home; nothing confines it to the worktree, as nothing confines `bash`. `read` and `edit` share the client tools' code: images come back as images, text as numbered lines, and `edit` refuses unless `old_string` appears exactly once. All three refuse a path that isn't a regular file (a FIFO would block), and `read` and `edit` refuse a file over 64 MiB, as `client_read` does. `write` creates parent directories and writes in place, so an existing file keeps its permissions.
|
|
|
|
|
Beside `bash`, agents get `read`, `write`, `edit`, `grep` and `list` MCP tools (`internal/agents/files.go`), which byre carries out itself, as the service user and outside the agent's sandbox, the same set the client Macs get as `client_read`, `client_write`, `client_edit`, `client_grep` and `client_list`. A path is relative to the task's worktree, absolute, or starts with `~/` for the service user's home; nothing confines it to the worktree, as nothing confines `bash`. `read` and `edit` share the client tools' code: images come back as images, text as numbered lines, and `edit` refuses unless `old_string` appears exactly once. All three refuse a path that isn't a regular file (a FIFO would block), and `read` and `edit` refuse a file over 64 MiB, as `client_read` does. `write` creates parent directories and writes in place, so an existing file keeps its permissions.
|
|
|
|
|
|
|
|
|
|
`grep` and `client_grep` run the same search, `internal/grep`, in-process on the server and as `byre grep [--glob GLOB] [--ignore-case] [--files-only] PATTERN PATH` on a client, from the `byre` binary every Byre.app carries. The pattern is Go RE2, matched line by line as each file streams through, so memory stays bounded; a line over 1 MiB isn't searched and prints as `path:line: line over 1 MiB, not searched`, and the deadline is checked before every read. A directory is walked recursively, without following symlinks, skipping `.git` entries, binary files (a NUL byte in the first 8000 bytes), unreadable files and whatever gitignore rules exclude: the repository's `info/exclude` (in a linked worktree, whose `.git` is a file, the one in the shared git directory its `gitdir` and `commondir` lead to) and every `.gitignore` from the repository root (the nearest ancestor holding `.git`, or the searched directory outside a repository) down, matched with go-git's `plumbing/format/gitignore`. The global `core.excludesFile` is not read. A `path` that is itself ignored, or inside an ignored directory, is searched anyway, minus what `.gitignore` files inside it exclude. `--glob` matches file names only. Matches print as `path:line:text`, the path as given joined with the file's path below it, lines cut at 500 bytes, and output stops at 64 KiB with a closing line saying so; no match prints `No matches.`. The server `grep` gives up after 2 minutes.
|
|
|
|
|
|
|
|
|
|
`list` and `client_list` run the same listing, `internal/dirlist`, in-process on the server (`path` defaults to the worktree) and as `byre list PATH` on a client. It reads one directory, not recursively, and prints each entry's name on its own line, sorted by name, with `/` after directories, following symlinks to decide; nothing is skipped, `.git` and gitignored entries included. An empty directory prints `<path> is empty.`, and output stops at 64 KiB with a line counting the entries left out.
|
|
|
|
|
|
|
|
|
|
The transcript shows a tool call under the name in its event, so a vendor's own tool appears under the vendor's name unless byre records it under Claude's (see "Agent sessions"); it labels every byre tool from its name (`mcp__byre__client_list` shows as `Client List`, `toolLabel` in `mac/Sources/Byre/TranscriptBuilder.swift`), and shows its `file_path`, or else its `path`, beside the label, relative to the worktree.
|
|
|
|
|
|
|
|
|
|
The **Terminal** tab (`mac/Sources/Byre/Terminal.swift`) draws it with SwiftTerm's `TerminalView`. The terminal uses the UI font and is coloured from the palette (background, text, accent cursor, accent-tint selection); ANSI colours are SwiftTerm's defaults. The colours are set once per light/dark scheme (`TerminalSession.scheme`), never on other SwiftUI updates, since each colour set makes SwiftTerm redraw the whole screen and the tab re-renders on every task change in the store. It fits the pane and resizes with it. Opening the tab, or selecting another task while it is open, opens one socket for the selected task; switching tab or task closes it, and the shell keeps running, so coming back replays its recent output. A socket that drops for any other reason reconnects after 2s while the task is active; after a byre restart that starts a new shell. When the shell exits the header shows `shell ended: <status>` and a **New shell** button; when it can't be started (the socket closes with 1011, the error in byre's log as `start terminal`), the header says so, with the same button to try again. Ctrl key combinations typed in the terminal go to the shell, not to byre's Ctrl-K, Ctrl-P, Ctrl-L and Ctrl-S shortcuts; ⌘K, ⌘P, ⌘L and ⌘S still work.
|
|
|
|
|
|
|
|
|
|
## Snapshots
|
|
|
|
|