Reconnect the app's command channel after sleep; list clients from it

This commit is contained in:
Ian Gulliver
2026-10-18 09:16:31 -07:00
parent 931d1b9a96
commit 1363a3604d
11 changed files with 444 additions and 388 deletions
+5 -4
View File
@@ -1787,17 +1787,18 @@ components:
sse_clients: { type: integer }
clients:
type: array
description: Each distinct user, Mac and app build with an open event stream, oldest connection first.
description: Each running app that holds its /api/clients/connect WebSocket, oldest connection first.
items: { $ref: "#/components/schemas/ConnectedClient" }
ConnectedClient:
type: object
required: [login, host, commit, connected_at]
required: [id, login, host, commit, connected_at]
properties:
id: { type: string, description: The ID the app made up when it started, from its Byre-Client header. }
login: { type: string }
host: { type: string, description: The Mac's name from its Byre-Host header; empty when the client sent none. }
host: { type: string, description: The Mac's name from its Byre-Host header. }
commit: { type: string, description: The git commit the app was built from, from its Byre-Commit header; empty when the client sent none. }
connected_at: { type: string, format: date-time, description: When the client's oldest open stream connected. }
connected_at: { type: string, format: date-time, description: When the client's WebSocket connected. }
last_action_at: { type: string, format: date-time, description: When byre last received a request the human made from this client since byre started; absent when none. }
ClientRequest:
+4 -4
View File
@@ -212,7 +212,7 @@ Codex runs with its own home, `codex` under the data directory (`CODEX_HOME`, se
## Status
`GET /api/admin/status` reports `running_turns` (task sessions now `running`, as session, task and vendor; lead turns aren't listed); `rate_limits`, the latest status each vendor sent since byre started (Claude's `rate_limit_info`, with `windowMins` added to each `unifiedWindows` entry by `addClaudeWindowMins` in `internal/agents/manager.go` from the window's name, since Claude doesn't send a length; Codex's `account/rateLimits/updated` reshaped by `codexRateLimit` in `internal/agents/codex.go` into the same `status`, `rateLimitType` and `unifiedWindows` fields, `windowMins` from its `windowDurationMins`). Claude only reports during its turns; Codex is also asked once at startup (`readCodexRateLimit`, a short-lived `codex app-server` that calls `account/rateLimits/read` without opening a thread), so its entry is there before any Codex turn runs, and every Codex update broadcasts `admin.health`; `agent_processes`, `zombies`, `stuck_after_seconds` and `protocol_errors` (below); `clients`, one entry per distinct login, `host` and `commit` among the open `GET /api/events` streams, with the oldest stream's `connected_at` (the app opens two streams, so it shows once). `host` and `commit` come from the `Byre-Host` and `Byre-Commit` headers, which the macOS app sends on every request: the Mac's name (`Host.current().localizedName`, else `ProcessInfo.processInfo.hostName`, the name agents' client tools take, see "Client Macs") and the `ByreCommit` that `mac/bundle.sh` stamped into its `Info.plist`; they are empty for a client that sends neither. To check that a rebuilt app is running, compare each client's `commit` with the commit it was built from. `last_action_at` is when byre last received a request the human made from that login, host and commit: any request other than a `GET`, except the two the app sends on its own (`PUT /api/me/pane-focus` and `PUT /api/tasks/{taskId}/summary-read`, listed in `automaticRequests` in `internal/server/server.go`), and every binary message on a terminal WebSocket, which includes the replies SwiftTerm sends by itself to queries in the output (cursor position and the like). The `identify` middleware and `terminalInput` record it in the hub's memory, so it is absent until that client's first action after byre starts. An action at least a second after the last one from that client that broadcast, and every `GET /api/events` stream opening or closing, broadcasts an unstored `admin.clients` event (`clientsChanged`). And server counts (start time, agent processes, projects, active tasks, SSE clients, database size including WAL, last event seq).
`GET /api/admin/status` reports `running_turns` (task sessions now `running`, as session, task and vendor; lead turns aren't listed); `rate_limits`, the latest status each vendor sent since byre started (Claude's `rate_limit_info`, with `windowMins` added to each `unifiedWindows` entry by `addClaudeWindowMins` in `internal/agents/manager.go` from the window's name, since Claude doesn't send a length; Codex's `account/rateLimits/updated` reshaped by `codexRateLimit` in `internal/agents/codex.go` into the same `status`, `rateLimitType` and `unifiedWindows` fields, `windowMins` from its `windowDurationMins`). Claude only reports during its turns; Codex is also asked once at startup (`readCodexRateLimit`, a short-lived `codex app-server` that calls `account/rateLimits/read` without opening a thread), so its entry is there before any Codex turn runs, and every Codex update broadcasts `admin.health`; `agent_processes`, `zombies`, `stuck_after_seconds` and `protocol_errors` (below); `clients`, one entry per command channel byre holds (the `/api/clients/connect` WebSockets in `internal/clients`, see "Client Macs"), oldest first, so it lists exactly the Macs `list_clients` and the `client_*` tools can reach; a Mac whose event streams are open but whose command channel is down isn't listed. `id`, `host` and `commit` come from the `Byre-Client`, `Byre-Host` and `Byre-Commit` headers, which the macOS app sends on every request: a UUID the app makes up each time it starts, the Mac's name (`Host.current().localizedName`, else `ProcessInfo.processInfo.hostName`, the name agents' client tools take) and the `ByreCommit` that `mac/bundle.sh` stamped into its `Info.plist`. To check that a rebuilt app is running, compare each client's `commit` with the commit it was built from. `last_action_at` is when byre last received a request the human made from that client `id`: any request other than a `GET`, except the two the app sends on its own (`PUT /api/me/pane-focus` and `PUT /api/tasks/{taskId}/summary-read`, listed in `automaticRequests` in `internal/server/server.go`), and every binary message on a terminal WebSocket, which includes the replies SwiftTerm sends by itself to queries in the output (cursor position and the like). The `identify` middleware and `terminalInput` record it in the client registry's memory (`Registry.Acted` in `internal/clients`), so it is absent until that client's first action after byre starts. The registry keeps each client by `id` across command-channel drops, so a Mac that sleeps and reconnects keeps its `last_action_at`, and forgets a client 24 hours after it was last connected or acted, since an app that quit never comes back under that `id`. An action at least a second after the last one from that client that broadcast, every `GET /api/events` stream opening or closing, and every command channel connecting or disconnecting broadcasts an unstored `admin.clients` event (`clientsChanged`). And server counts (start time, agent processes, projects, active tasks, SSE clients, database size including WAL, last event seq).
Agent health is `internal/agents/health.go`:
@@ -315,11 +315,11 @@ Answers reach the agent as one message, listing each answer under the question o
## Client Macs
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 client is named by its `Byre-Host` header (required), and a newer connection from the same host replaces the older one. byre keeps the connections in memory (`internal/clients`), pinging each every 30s and dropping one that doesn't answer within 15s. 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".
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 eight 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 connected clients, with each one's login, connection time and app commit, and its `last_action_at` from Status (see "Status") matched on login, host and commit; the most recently active is listed first and marked.
- `list_clients`: the status's `clients` (see "Status"), with each one's host, login, `id`, connection time, app commit and `last_action_at`; 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.
- `client_upload`: `op: upload`. `path` is checked against the worktree through an `os.Root`; byre registers a transfer naming the path's directory and base name under a random id for as long as the call waits (at most 10 minutes), and the app downloads `GET /api/clients/transfers/{transferId}`, which streams `tar -czf - -C <dir> -- <name>` (404 once the call has ended). The app unpacks it into a new `.byre-upload-<uuid>` directory beside `dest` (`~/` expanded, parent directories created), removes whatever is at `dest`, moves the archive's one entry there and removes the temporary directory.
- `client_download`: `op: download`, the reverse. byre makes `dest`'s parent directories through an `os.Root` on the worktree and registers a receiving transfer (`Transfer.Receive`) for a fresh temporary directory; the app runs `tar -czf <temp file> -C <path's parent> -- <path's name>` and `PUT`s it to `/api/clients/transfers/{transferId}`, which writes it once (`O_EXCL`) and answers 204, and 404 for a sending transfer, one already received or one whose call has ended (`GET` likewise answers 404 for a receiving one). byre unpacks it into a new `.byre-download-<transfer id>` directory beside `dest`, removes whatever is at `dest`, renames the archive's one entry there and removes the temporary directories, all inside the `os.Root`.
@@ -332,7 +332,7 @@ Implementer sessions get eight MCP tools for it (`internal/agents/clients.go`).
A byre restart drops every connection; each app reconnects 2s later, and calls in flight at the restart fail as disconnected. Updating a client's app goes through the tools themselves: `client_upload` the new `byre.app`, then `client_bash` a command that quits the old app and opens the new one, which outlives the app and reconnects. An app that doesn't know the `upload` op can't be updated this way, so its update is by hand.
`log show --last 10m --predicate 'subsystem == "run.fc.byre.mac"'` on a client shows `client request <id>: <op>` lines, connection drops and `byre-hands` starts and exits; byre's log shows `client connected` and `client disconnected`.
`log show --info --last 10m --predicate 'subsystem == "run.fc.byre.mac"'` on a client shows `client request <id>: <op>` lines and `byre-hands` starts and exits, which are info level and kept only in memory, so they cover the last few minutes at most; connection drops (`client connection closed`, `client connection got no pong`) are notices, which macOS keeps on disk, so `log show --start` reaches much further back for them; byre's log shows `client connected` and `client disconnected`.
## Changes
+15 -28
View File
@@ -35,7 +35,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
{
Name: "list_clients",
Description: "List the Macs with byre's app open: the human's own machines, not the one byre (and your worktree) runs on. " +
"Each has its name, which the other client tools take as client, the human using it, and when the human last did something in byre from it. " +
"Each has its name, which the other client tools take as client, the id of that running app (pass it as client instead when two apps are open on one Mac), the human using it, and when the human last did something in byre from it. " +
"The most recently active one is listed first; it is usually the Mac the human sits at.",
InputSchema: json.RawMessage(`{"type": "object", "properties": {}}`),
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
@@ -50,7 +50,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients."},
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
"command": {"type": "string"},
"timeout_ms": {"type": "integer", "description": "Kill the command after this long (default 120000, at most 600000)."}
},
@@ -67,7 +67,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients."},
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
"path": {"type": "string", "description": "The file or directory, relative to your worktree."},
"dest": {"type": "string", "description": "Its new path on the client, absolute or starting with ~/, e.g. ~/Applications/byre.app."}
},
@@ -84,7 +84,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients."},
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
"path": {"type": "string", "description": "The file or directory on the client, absolute or starting with ~/."},
"dest": {"type": "string", "description": "Its new path, relative to your worktree; parent directories are created."}
},
@@ -101,7 +101,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients."},
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
"path": {"type": "string", "description": "The file, absolute or starting with ~/."},
"offset": {"type": "integer", "description": "The line number to start from (default 1)."},
"limit": {"type": "integer", "description": "The most lines to return (default 2000)."}
@@ -118,7 +118,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients."},
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
"path": {"type": "string", "description": "The file, absolute or starting with ~/."},
"content": {"type": "string"}
},
@@ -135,7 +135,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients."},
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
"path": {"type": "string", "description": "The file, absolute or starting with ~/."},
"old_string": {"type": "string"},
"new_string": {"type": "string"}
@@ -156,7 +156,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients."},
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
"tool": {"type": "string", "enum": ["screen_list", "capture", "ax_tree", "act", "browser", "chrome"]},
"arguments": {"type": "object", "description": "The tool's arguments."}
},
@@ -174,39 +174,26 @@ func (m *Manager) listClients(ctx context.Context) (string, error) {
if err != nil {
return "", err
}
type row struct {
c clients.Client
last *time.Time
}
rows := []row{}
for _, c := range m.clients.List() {
r := row{c: c}
for _, s := range status.Clients {
if s.Login == c.Login && s.Host == c.Host && s.Commit == c.Commit {
r.last = s.LastActionAt
}
}
rows = append(rows, r)
}
rows := status.Clients
if len(rows) == 0 {
return "No client Macs are connected.", nil
}
sort.SliceStable(rows, func(i, j int) bool {
a, b := rows[i].last, rows[j].last
a, b := rows[i].LastActionAt, rows[j].LastActionAt
return a != nil && (b == nil || a.After(*b))
})
now := time.Now()
b := &strings.Builder{}
for i, r := range rows {
fmt.Fprintf(b, "- %s (%s): ", r.c.Host, r.c.Login)
if r.last == nil {
fmt.Fprintf(b, "- %s (%s, id %s): ", r.Host, r.Login, r.Id)
if r.LastActionAt == nil {
b.WriteString("no action since byre started")
} else {
fmt.Fprintf(b, "last action %s ago", now.Sub(*r.last).Round(time.Second))
fmt.Fprintf(b, "last action %s ago", now.Sub(*r.LastActionAt).Round(time.Second))
}
fmt.Fprintf(b, ", connected %s ago, app built from %s", now.Sub(r.c.ConnectedAt).Round(time.Second), r.c.Commit)
if i == 0 && r.last != nil {
fmt.Fprintf(b, ", connected %s ago, app built from %s", now.Sub(r.ConnectedAt).Round(time.Second), r.Commit)
if i == 0 && r.LastActionAt != nil {
b.WriteString(" (most recently active)")
}
b.WriteString("\n")
+253 -250
View File
@@ -736,12 +736,15 @@ type ConnectedClient struct {
// Commit The git commit the app was built from
Commit string `json:"commit"`
// ConnectedAt When the client's oldest open stream connected.
// ConnectedAt When the client's WebSocket connected.
ConnectedAt time.Time `json:"connected_at"`
// Host The Mac's name from its Byre-Host header; empty when the client sent none.
// Host The Mac's name from its Byre-Host header.
Host string `json:"host"`
// Id The ID the app made up when it started
Id string `json:"id"`
// LastActionAt When byre last received a request the human made from this client since byre started; absent when none.
LastActionAt *time.Time `json:"last_action_at,omitempty"`
Login string `json:"login"`
@@ -1164,7 +1167,7 @@ type Status struct {
// AgentProcesses Every agent process byre is tracking, idle sessions included.
AgentProcesses []AgentProcess `json:"agent_processes"`
// Clients Each distinct user, Mac and app build with an open event stream, oldest connection first.
// Clients Each running app that holds its /api/clients/connect WebSocket, oldest connection first.
Clients []ConnectedClient `json:"clients"`
DbBytes int64 `json:"db_bytes"`
LastEventSeq int64 `json:"last_event_seq"`
@@ -3800,253 +3803,253 @@ 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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",
}
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
+97 -32
View File
@@ -22,13 +22,16 @@ const (
readLimit = 256 << 20
pingInterval = 30 * time.Second
pingTimeout = 15 * time.Second
expireAfter = 24 * time.Hour
)
type Client struct {
Login string
Host string
Commit string
ConnectedAt time.Time
ID string
Login string
Host string
Commit string
ConnectedAt time.Time
LastActionAt *time.Time
}
type Transfer struct {
@@ -38,44 +41,91 @@ type Transfer struct {
}
type conn struct {
Client
ws *websocket.Conn
done chan struct{}
mu sync.Mutex
pending map[string]chan api.ClientReply
}
type entry struct {
Client
conn *conn
seen time.Time
notified time.Time
}
type Registry struct {
mu sync.Mutex
conns map[string]*conn
clients map[string]*entry
transfers map[string]Transfer
changed func()
}
func New() *Registry {
return &Registry{conns: map[string]*conn{}, transfers: map[string]Transfer{}}
func New(changed func()) *Registry {
return &Registry{clients: map[string]*entry{}, transfers: map[string]Transfer{}, changed: changed}
}
func (r *Registry) Serve(ctx context.Context, ws *websocket.Conn, client Client) {
func (r *Registry) entry(id, login, host, commit string, now time.Time) *entry {
for k, e := range r.clients {
if e.conn == nil && now.Sub(e.seen) > expireAfter {
delete(r.clients, k)
}
}
e := r.clients[id]
if e == nil {
e = &entry{Client: Client{ID: id}}
r.clients[id] = e
}
e.Login = login
e.Host = host
e.Commit = commit
e.seen = now
return e
}
func (r *Registry) Acted(id, login, host, commit string) bool {
if id == "" {
return false
}
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
e := r.entry(id, login, host, commit, now)
e.LastActionAt = &now
if now.Sub(e.notified) < time.Second {
return false
}
e.notified = now
return true
}
func (r *Registry) Serve(ctx context.Context, ws *websocket.Conn, id, login, host, commit string) {
ws.SetReadLimit(readLimit)
c := &conn{Client: client, ws: ws, done: make(chan struct{}), pending: map[string]chan api.ClientReply{}}
c := &conn{ws: ws, done: make(chan struct{}), pending: map[string]chan api.ClientReply{}}
r.mu.Lock()
old := r.conns[client.Host]
r.conns[client.Host] = c
now := time.Now().UTC()
e := r.entry(id, login, host, commit, now)
old := e.conn
e.conn = c
e.ConnectedAt = now
r.mu.Unlock()
if old != nil {
old.ws.Close(websocket.StatusGoingAway, "replaced by a newer connection from this host")
old.ws.Close(websocket.StatusGoingAway, "replaced by a newer connection from this client")
}
slog.Info("client connected", "host", client.Host, "login", client.Login, "commit", client.Commit)
slog.Info("client connected", "id", id, "host", host, "login", login, "commit", commit)
r.changed()
defer func() {
r.mu.Lock()
if r.conns[client.Host] == c {
delete(r.conns, client.Host)
if e.conn == c {
e.conn = nil
e.seen = time.Now()
}
r.mu.Unlock()
close(c.done)
slog.Info("client disconnected", "host", client.Host, "login", client.Login)
slog.Info("client disconnected", "id", id, "host", host, "login", login)
r.changed()
}()
ctx, cancel := context.WithCancel(ctx)
@@ -98,7 +148,7 @@ func (r *Registry) Serve(ctx context.Context, ws *websocket.Conn, client Client)
delete(c.pending, reply.Id)
c.mu.Unlock()
if ch == nil {
slog.Warn("client reply for no request", "host", client.Host, "id", reply.Id)
slog.Warn("client reply for no request", "host", host, "id", reply.Id)
continue
}
ch <- reply
@@ -128,25 +178,40 @@ func (r *Registry) List() []Client {
r.mu.Lock()
defer r.mu.Unlock()
out := []Client{}
for _, c := range r.conns {
out = append(out, c.Client)
for _, e := range r.clients {
if e.conn != nil {
out = append(out, e.Client)
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Host < out[j].Host })
sort.Slice(out, func(i, j int) bool { return out[i].ConnectedAt.Before(out[j].ConnectedAt) })
return out
}
func (r *Registry) Call(ctx context.Context, host string, req api.ClientRequest) (api.ClientReply, error) {
func (r *Registry) Call(ctx context.Context, name string, req api.ClientRequest) (api.ClientReply, error) {
r.mu.Lock()
c := r.conns[host]
hosts := []string{}
for h := range r.conns {
hosts = append(hosts, h)
matches := []*conn{}
ids := []string{}
connected := []string{}
for _, e := range r.clients {
if e.conn == nil {
continue
}
connected = append(connected, fmt.Sprintf("%s (%s)", e.Host, e.ID))
if e.ID == name || e.Host == name {
matches = append(matches, e.conn)
ids = append(ids, e.ID)
}
}
r.mu.Unlock()
if c == nil {
slices.Sort(hosts)
return api.ClientReply{}, fmt.Errorf("no client named %q is connected; connected: %s", host, strings.Join(hosts, ", "))
slices.Sort(connected)
if len(matches) == 0 {
return api.ClientReply{}, fmt.Errorf("no client named %q is connected; connected: %s", name, strings.Join(connected, ", "))
}
if len(matches) > 1 {
slices.Sort(ids)
return api.ClientReply{}, fmt.Errorf("%d clients on %s are connected; name one by its id: %s", len(matches), name, strings.Join(ids, ", "))
}
c := matches[0]
req.Id = rand.Text()
ch := make(chan api.ClientReply, 1)
@@ -165,7 +230,7 @@ func (r *Registry) Call(ctx context.Context, host string, req api.ClientRequest)
}
err = c.ws.Write(ctx, websocket.MessageText, data)
if err != nil {
return api.ClientReply{}, fmt.Errorf("send to %s: %w", host, err)
return api.ClientReply{}, fmt.Errorf("send to %s: %w", name, err)
}
select {
@@ -175,9 +240,9 @@ func (r *Registry) Call(ctx context.Context, host string, req api.ClientRequest)
}
return reply, nil
case <-c.done:
return api.ClientReply{}, fmt.Errorf("%s disconnected before replying", host)
return api.ClientReply{}, fmt.Errorf("%s disconnected before replying", name)
case <-ctx.Done():
return api.ClientReply{}, fmt.Errorf("%s didn't reply: %w", host, ctx.Err())
return api.ClientReply{}, fmt.Errorf("%s didn't reply: %w", name, ctx.Err())
}
}
+4 -11
View File
@@ -10,17 +10,15 @@ import (
"os"
"os/exec"
"path/filepath"
"time"
"github.com/coder/websocket"
"g.fc.run/misc/byre/internal/clients"
)
func (s *Server) ConnectClient(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("Byre-Client")
host := r.Header.Get("Byre-Host")
if host == "" {
writeError(w, http.StatusBadRequest, errors.New("missing Byre-Host"))
if id == "" || host == "" {
writeError(w, http.StatusBadRequest, errors.New("missing Byre-Client or Byre-Host"))
return
}
@@ -31,12 +29,7 @@ func (s *Server) ConnectClient(w http.ResponseWriter, r *http.Request) {
}
defer ws.CloseNow()
s.clients.Serve(r.Context(), ws, clients.Client{
Login: actor(r),
Host: host,
Commit: r.Header.Get("Byre-Commit"),
ConnectedAt: time.Now().UTC(),
})
s.clients.Serve(r.Context(), ws, id, actor(r), host, r.Header.Get("Byre-Commit"))
}
func (s *Server) GetClientTransfer(w http.ResponseWriter, r *http.Request, transferID string) {
+5 -49
View File
@@ -1,45 +1,25 @@
package server
import (
"sort"
"sync"
"time"
"g.fc.run/misc/byre/internal/api"
)
type clientKey struct{ login, host, commit string }
type hub struct {
mu sync.Mutex
subs map[chan api.Event]api.ConnectedClient
lastAction map[clientKey]time.Time
lastNotified map[clientKey]time.Time
mu sync.Mutex
subs map[chan api.Event]bool
}
func newHub() *hub {
return &hub{subs: map[chan api.Event]api.ConnectedClient{}, lastAction: map[clientKey]time.Time{}, lastNotified: map[clientKey]time.Time{}}
return &hub{subs: map[chan api.Event]bool{}}
}
func (h *hub) acted(login, host, commit string) bool {
h.mu.Lock()
defer h.mu.Unlock()
k := clientKey{login, host, commit}
now := time.Now()
h.lastAction[k] = now
prev, ok := h.lastNotified[k]
if ok && now.Sub(prev) < time.Second {
return false
}
h.lastNotified[k] = now
return true
}
func (h *hub) subscribe(client api.ConnectedClient) chan api.Event {
func (h *hub) subscribe() chan api.Event {
h.mu.Lock()
defer h.mu.Unlock()
ch := make(chan api.Event, 256)
h.subs[ch] = client
h.subs[ch] = true
return ch
}
@@ -72,27 +52,3 @@ func (h *hub) count() int {
defer h.mu.Unlock()
return len(h.subs)
}
func (h *hub) clients() []api.ConnectedClient {
h.mu.Lock()
defer h.mu.Unlock()
oldest := map[clientKey]api.ConnectedClient{}
for _, c := range h.subs {
k := clientKey{c.Login, c.Host, c.Commit}
prev, ok := oldest[k]
if ok && !c.ConnectedAt.Before(prev.ConnectedAt) {
continue
}
oldest[k] = c
}
out := []api.ConnectedClient{}
for k, c := range oldest {
at, ok := h.lastAction[k]
if ok {
c.LastActionAt = &at
}
out = append(out, c)
}
sort.Slice(out, func(i, j int) bool { return out[i].ConnectedAt.Before(out[j].ConnectedAt) })
return out
}
+18 -3
View File
@@ -45,11 +45,11 @@ func New(dataDir string, portFirst int) (*Server, error) {
s := &Server{
hub: newHub(),
clients: clients.New(),
terminals: newTerminals(),
startedAt: time.Now().UTC(),
users: map[string]string{},
}
s.clients = clients.New(s.clientsChanged)
s.store, err = store.Open(filepath.Join(dataDir, "byre.db"), portFirst, s.publish)
if err != nil {
@@ -145,11 +145,26 @@ func (s *Server) identify(next http.Handler) http.Handler {
}
func (s *Server) acted(login string, r *http.Request) {
if s.hub.acted(login, r.Header.Get("Byre-Host"), r.Header.Get("Byre-Commit")) {
if s.clients.Acted(r.Header.Get("Byre-Client"), login, r.Header.Get("Byre-Host"), r.Header.Get("Byre-Commit")) {
s.clientsChanged()
}
}
func (s *Server) connectedClients() []api.ConnectedClient {
out := []api.ConnectedClient{}
for _, c := range s.clients.List() {
out = append(out, api.ConnectedClient{
Id: c.ID,
Login: c.Login,
Host: c.Host,
Commit: c.Commit,
ConnectedAt: c.ConnectedAt,
LastActionAt: c.LastActionAt,
})
}
return out
}
func (s *Server) clientsChanged() {
s.store.Broadcast(api.Event{Kind: "admin.clients", Actor: "byre", Payload: map[string]any{}})
}
@@ -638,7 +653,7 @@ func (s *Server) status(ctx context.Context) (api.Status, error) {
Projects: c.Projects,
ActiveTasks: c.ActiveTasks,
SseClients: s.hub.count(),
Clients: s.hub.clients(),
Clients: s.connectedClients(),
LiveAgents: s.agents.LiveCount(),
RunningTurns: usage.Running,
RateLimits: usage.RateLimits,
+1 -6
View File
@@ -55,12 +55,7 @@ func (s *Server) StreamEvents(w http.ResponseWriter, r *http.Request, params api
w.Header().Set("Cache-Control", "no-cache")
w.WriteHeader(http.StatusOK)
live := s.hub.subscribe(api.ConnectedClient{
Login: viewer,
Host: r.Header.Get("Byre-Host"),
Commit: r.Header.Get("Byre-Commit"),
ConnectedAt: time.Now(),
})
live := s.hub.subscribe()
defer s.clientsChanged()
defer s.hub.unsubscribe(live)
s.clientsChanged()
+3
View File
@@ -95,6 +95,8 @@ func errorText(_ error: any Error) -> String {
return String(describing: e)
}
private let clientID = UUID().uuidString
final class API: Sendable {
let origin: URL
let session: URLSession
@@ -104,6 +106,7 @@ final class API: Sendable {
self.origin = origin
let config = URLSessionConfiguration.default
config.httpAdditionalHeaders = [
"Byre-Client": clientID,
"Byre-Commit": Bundle.main.object(forInfoDictionaryKey: "ByreCommit") as! String,
"Byre-Host": Host.current().localizedName ?? ProcessInfo.processInfo.hostName,
]
+39 -1
View File
@@ -7,6 +7,8 @@ typealias ClientRequest = Components.Schemas.ClientRequest
typealias ClientReply = Components.Schemas.ClientReply
private let retryAfter = Duration.seconds(2)
private let checkEvery = Duration.seconds(10)
private let staleAfter: TimeInterval = 25
private let maxOutput = 64 << 10
private let maxMessage = 256 << 20
private let maxRead = 64 << 20
@@ -18,6 +20,8 @@ final class Remote {
private let hands = Hands()
private var ws: URLSessionWebSocketTask?
private var stopped = false
private var heard = Date()
private var checker: Task<Void, Never>?
init(api: API) {
self.api = api
@@ -30,12 +34,22 @@ final class Remote {
let task = api.session.webSocketTask(with: api.url("/api/clients/connect", scheme: "wss"))
task.maximumMessageSize = maxMessage
ws = task
heard = Date()
task.resume()
receive(task)
if checker == nil {
checker = Task { [weak self] in
while !Task.isCancelled {
try? await Task.sleep(for: checkEvery)
self?.check()
}
}
}
}
func stop() {
stopped = true
checker?.cancel()
ws?.cancel(with: .normalClosure, reason: nil)
ws = nil
Task { [hands] in
@@ -43,6 +57,30 @@ final class Remote {
}
}
// A socket byre closed while this Mac slept stays open here until something is sent on it.
private func check() {
guard let task = ws else {
return
}
if Date().timeIntervalSince(heard) >= staleAfter {
log.notice("client connection got no pong for \(Int(staleAfter))s, reconnecting")
ws = nil
task.cancel(with: .goingAway, reason: nil)
start()
return
}
task.sendPing { error in
guard error == nil else {
return
}
Task { @MainActor in
if self.ws === task {
self.heard = Date()
}
}
}
}
private func receive(_ task: URLSessionWebSocketTask) {
task.receive { result in
Task { @MainActor in
@@ -59,7 +97,7 @@ final class Remote {
case .success:
self.receive(task)
case .failure(let error):
log.info("client connection closed: \(errorText(error), privacy: .public)")
log.notice("client connection closed:\(errorText(error), privacy: .public)")
self.ws = nil
try? await Task.sleep(for: retryAfter)
self.start()