Let task agents use client Macs directly: bash, copy and hands

This commit is contained in:
Ian Gulliver
2026-10-17 23:45:31 -07:00
parent 8d9ba10849
commit 9b79ade073
25 changed files with 1382 additions and 952 deletions
-191
View File
@@ -1,191 +0,0 @@
package agents
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"g.fc.run/misc/byre/internal/api"
"g.fc.run/misc/byre/internal/mcp"
"g.fc.run/misc/byre/internal/store"
)
const (
bundleTimeout = 5 * time.Minute
MaxRunOutput = 64 << 10
)
type runOnClientArgs struct {
Paths []string `json:"paths"`
Command string `json:"command"`
Why string `json:"why"`
}
func (m *Manager) runOnClientTool(l *live) mcp.Tool {
return mcp.Tool{
Name: "run_on_client",
Description: "Ask the human to run a command on their own Mac, with files from your worktree, for example to install and launch an app you built. " +
"byre packs paths into a bundle now, so later changes to them aren't sent, and files a card in the human's inbox showing why, the command and the files. " +
"Every Mac the human has byre open on shows the card with Run and Dismiss; each Mac where they click Run downloads the bundle, unpacks it into a new empty directory, " +
"and runs command there with zsh as a login shell, as the human. The directory is deleted once the command finishes, so copy anything that must stay elsewhere first. " +
"The command keeps running if it quits byre's app, and the app reports it when it next starts. " +
"This call returns at once; you don't need to end your turn. Each Mac's run reaches you as a message with its host name, exit status and the last 64 KiB of its output. " +
"The card stays until the human dismisses it, so it can be run again or on another Mac.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"paths": {"type": "array", "items": {"type": "string"}, "description": "Files or directories to send, relative to your worktree; each is unpacked at the same relative path. May be empty."},
"command": {"type": "string", "description": "The zsh command to run in the directory the bundle unpacks into, e.g. 'rm -rf ~/Applications/byre.app && ditto mac/.build/byre.app ~/Applications/byre.app && (pkill -x Byre; open ~/Applications/byre.app)'."},
"why": {"type": "string", "description": "One or two sentences for the human: what running it does and why."}
},
"required": ["paths", "command", "why"]
}`),
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
return m.runOnClient(ctx, l, raw)
},
}
}
func (m *Manager) runOnClient(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
args := runOnClientArgs{}
err := json.Unmarshal(raw, &args)
if err != nil {
return "", fmt.Errorf("malformed run_on_client call: %w", err)
}
if strings.TrimSpace(args.Command) == "" {
return "", errors.New("command is empty")
}
if strings.TrimSpace(args.Why) == "" {
return "", errors.New("why is empty: say what running it does and why")
}
paths, err := checkBundlePaths(l.dir, args.Paths)
if err != nil {
return "", err
}
id := store.NewID()
size, err := m.writeBundle(ctx, l.dir, paths, m.store.BundlePath(id))
if err != nil {
return "", err
}
payload := map[string]any{
"why": args.Why,
"command": args.Command,
"paths": paths,
"bytes": size,
}
err = m.store.CreateInboxItem(ctx, m.newItem(l, id, id, api.ClientRun, payload))
if err != nil {
os.Remove(m.store.BundlePath(id))
return "", err
}
return fmt.Sprintf("Run request %s filed in the human's inbox with a %d-byte bundle. Each Mac's run will reach you as a message.", id, size), nil
}
func checkBundlePaths(dir string, paths []string) ([]string, error) {
root, err := os.OpenRoot(dir)
if err != nil {
return nil, err
}
defer root.Close()
out := []string{}
for _, p := range paths {
clean := filepath.Clean(p)
if filepath.IsAbs(clean) || clean == "." || strings.HasPrefix(clean, "-") {
return nil, fmt.Errorf("path %q must be relative to your worktree and inside it", p)
}
_, err = root.Lstat(clean)
if err != nil {
return nil, fmt.Errorf("path %q: %w", p, err)
}
out = append(out, clean)
}
return out, nil
}
func (m *Manager) writeBundle(ctx context.Context, dir string, paths []string, dest string) (int64, error) {
err := os.MkdirAll(filepath.Dir(dest), 0o755)
if err != nil {
return 0, err
}
tmp := dest + ".tmp"
cctx, cancel := context.WithTimeout(ctx, bundleTimeout)
defer cancel()
// -T /dev/null lets tar write an empty archive when paths is empty.
cmd := exec.CommandContext(cctx, "tar", append([]string{"-czf", tmp, "-C", dir, "-T", "/dev/null", "--"}, paths...)...)
out, err := cmd.CombinedOutput()
if err != nil {
os.Remove(tmp)
return 0, fmt.Errorf("pack bundle: %w: %s", err, out)
}
err = os.Rename(tmp, dest)
if err != nil {
return 0, err
}
info, err := os.Stat(dest)
if err != nil {
return 0, err
}
return info.Size(), nil
}
func (m *Manager) ReportRun(ctx context.Context, user, itemID string, report api.RunReport) (api.InboxItem, error) {
item, err := m.store.GetInboxItem(ctx, itemID)
if err != nil {
return item, err
}
if item.Kind != api.ClientRun {
return item, fmt.Errorf("%w: item %s is a %s, not a client_run", ErrInvalid, itemID, item.Kind)
}
client := strings.TrimSpace(report.Client)
if client == "" {
return item, fmt.Errorf("%w: client is empty", ErrInvalid)
}
if strings.TrimSpace(report.RunId) == "" {
return item, fmt.Errorf("%w: run_id is empty", ErrInvalid)
}
output := report.Output
if len(output) > MaxRunOutput {
output = strings.ToValidUTF8(output[len(output)-MaxRunOutput:], "")
}
resp := api.InboxResponse{
Id: report.RunId,
ItemId: itemID,
User: user,
Type: api.Run,
Text: &output,
Client: &client,
ExitCode: &report.ExitCode,
CreatedAt: time.Now().UTC(),
}
added, err := m.store.AddRunResponse(ctx, resp)
if err != nil {
return item, err
}
if !added {
slog.Info("client run already reported", "item", itemID, "response", resp.Id, "client", client)
return m.store.GetInboxItem(ctx, itemID)
}
slog.Info("client run reported", "item", itemID, "response", resp.Id, "user", user, "client", client, "exit", report.ExitCode)
go func() {
err := m.Deliver(context.Background(), item.AgentSessionId)
if err != nil {
slog.Error("deliver run report", "session", item.AgentSessionId, "err", err)
}
}()
return m.store.GetInboxItem(ctx, itemID)
}
+255
View File
@@ -0,0 +1,255 @@
package agents
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"g.fc.run/misc/byre/internal/api"
"g.fc.run/misc/byre/internal/clients"
"g.fc.run/misc/byre/internal/mcp"
)
const (
defaultBashTimeout = 2 * time.Minute
maxBashTimeout = 10 * time.Minute
replyGrace = 30 * time.Second
copyTimeout = 10 * time.Minute
handsTimeout = 11 * time.Minute
)
func (m *Manager) clientTools(l *live) []mcp.Tool {
return []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. " +
"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) {
return m.listClients(ctx)
},
},
{
Name: "client_bash",
Description: "Run a shell command on a client Mac, as the human, with zsh as a login shell in their home directory, and get its exit status and output (stdout and stderr together, the last 64 KiB). " +
"It runs at once, with no approval. The command runs in its own process group, which is killed at the timeout. " +
"A command that quits byre's app on that Mac, for example to install a new build, ends this call with the client disconnecting, but the command keeps running.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients."},
"command": {"type": "string"},
"timeout_ms": {"type": "integer", "description": "Kill the command after this long (default 120000, at most 600000)."}
},
"required": ["client", "command"]
}`),
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
return m.clientBash(ctx, raw)
},
},
{
Name: "client_copy",
Description: "Copy a file or directory from your worktree to a client Mac, replacing whatever is at dest there. " +
"Directories, app bundles included, are copied whole with their permissions and symlinks.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients."},
"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."}
},
"required": ["client", "path", "dest"]
}`),
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
return m.clientCopy(ctx, l, raw)
},
},
{
Name: "client_hands",
Description: "Call one of your hands tools (screen_list, capture, ax_tree, act, browser) on a client Mac instead of the Mac byre runs on: " +
"tool is the tool's name and arguments exactly what that tool takes. Window ids, refs and displays are that Mac's, so start with screen_list there. " +
"Every agent using that Mac shares one set of refs, so read the tree again if another agent may have. " +
"A call fails until the human has given byre's app Screen Recording and Accessibility (and Automation of Orion, for browser) on that Mac; say so when it does.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"client": {"type": "string", "description": "The client's name from list_clients."},
"tool": {"type": "string", "enum": ["screen_list", "capture", "ax_tree", "act", "browser"]},
"arguments": {"type": "object", "description": "The tool's arguments."}
},
"required": ["client", "tool"]
}`),
Rich: func(ctx context.Context, raw json.RawMessage) mcp.Result {
return m.clientHands(ctx, raw)
},
},
}
}
func (m *Manager) listClients(ctx context.Context) (string, error) {
status, err := m.status(ctx)
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)
}
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
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 {
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, ", connected %s ago, app built from %s", now.Sub(r.c.ConnectedAt).Round(time.Second), r.c.Commit)
if i == 0 && r.last != nil {
b.WriteString(" (most recently active)")
}
b.WriteString("\n")
}
return b.String(), nil
}
type clientBashArgs struct {
Client string `json:"client"`
Command string `json:"command"`
TimeoutMs *int `json:"timeout_ms"`
}
func (m *Manager) clientBash(ctx context.Context, raw json.RawMessage) (string, error) {
args := clientBashArgs{}
err := json.Unmarshal(raw, &args)
if err != nil {
return "", fmt.Errorf("malformed client_bash call: %w", err)
}
if strings.TrimSpace(args.Command) == "" {
return "", errors.New("command is empty")
}
timeout := defaultBashTimeout
if args.TimeoutMs != nil {
timeout = time.Duration(*args.TimeoutMs) * time.Millisecond
}
if timeout <= 0 || timeout > maxBashTimeout {
return "", fmt.Errorf("timeout_ms must be between 1 and %d", maxBashTimeout.Milliseconds())
}
ms := int(timeout.Milliseconds())
cctx, cancel := context.WithTimeout(ctx, timeout+replyGrace)
defer cancel()
reply, err := m.clients.Call(cctx, args.Client, api.ClientRequest{Op: api.Bash, Command: &args.Command, TimeoutMs: &ms})
if err != nil {
return "", err
}
if reply.ExitCode == nil || reply.Output == nil {
return "", fmt.Errorf("%s's reply has no exit_code or output", args.Client)
}
status := fmt.Sprintf("exit status %d", *reply.ExitCode)
if *reply.ExitCode == -1 {
status = fmt.Sprintf("killed after %s", timeout)
}
return status + "\n" + *reply.Output, nil
}
type clientCopyArgs struct {
Client string `json:"client"`
Path string `json:"path"`
Dest string `json:"dest"`
}
func (m *Manager) clientCopy(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
args := clientCopyArgs{}
err := json.Unmarshal(raw, &args)
if err != nil {
return "", fmt.Errorf("malformed client_copy call: %w", err)
}
if !filepath.IsAbs(args.Dest) && !strings.HasPrefix(args.Dest, "~/") {
return "", fmt.Errorf("dest %q must be absolute or start with ~/", args.Dest)
}
clean := filepath.Clean(args.Path)
if filepath.IsAbs(clean) {
return "", fmt.Errorf("path %q must be relative to your worktree", args.Path)
}
root, err := os.OpenRoot(l.dir)
if err != nil {
return "", err
}
_, err = root.Lstat(clean)
root.Close()
if err != nil {
return "", fmt.Errorf("path %q: %w", args.Path, err)
}
full := filepath.Join(l.dir, clean)
id := m.clients.AddTransfer(clients.Transfer{Dir: filepath.Dir(full), Name: filepath.Base(full)})
defer m.clients.RemoveTransfer(id)
cctx, cancel := context.WithTimeout(ctx, copyTimeout)
defer cancel()
_, err = m.clients.Call(cctx, args.Client, api.ClientRequest{Op: api.Copy, Transfer: &id, Dest: &args.Dest})
if err != nil {
return "", err
}
return fmt.Sprintf("Copied %s to %s on %s.", clean, args.Dest, args.Client), nil
}
type clientHandsArgs struct {
Client string `json:"client"`
Tool string `json:"tool"`
Arguments *map[string]any `json:"arguments"`
}
func (m *Manager) clientHands(ctx context.Context, raw json.RawMessage) mcp.Result {
args := clientHandsArgs{}
err := json.Unmarshal(raw, &args)
if err != nil {
return mcp.TextResult(fmt.Sprintf("malformed client_hands call: %s", err), true)
}
if args.Arguments == nil {
args.Arguments = &map[string]any{}
}
cctx, cancel := context.WithTimeout(ctx, handsTimeout)
defer cancel()
reply, err := m.clients.Call(cctx, args.Client, api.ClientRequest{Op: api.Hands, Tool: &args.Tool, Arguments: args.Arguments})
if err != nil {
return mcp.TextResult(err.Error(), true)
}
if reply.Result == nil {
return mcp.TextResult(fmt.Sprintf("%s's reply has no result", args.Client), true)
}
res := mcp.Result{}
err = remarshal(*reply.Result, &res)
if err != nil {
return mcp.TextResult(fmt.Sprintf("%s's result isn't an MCP tool result: %s", args.Client, err), true)
}
return res
}
-7
View File
@@ -492,16 +492,9 @@ func deliveryMessage(pending []store.Pending) string {
fmt.Fprintf(b, "Your error %s: %s\n", p.Item.Id, p.Item.Payload["error"])
case api.RateLimited:
fmt.Fprintf(b, "Your rate-limited turn %s: %s\n", p.Item.Id, p.Item.Payload["error"])
case api.ClientRun:
fmt.Fprintf(b, "Your run request %s: %s\n", p.Item.Id, p.Item.Payload["command"])
}
switch r.Type {
case api.Run:
fmt.Fprintf(b, "%s ran it on %s: exit status %d\n", r.User, *r.Client, *r.ExitCode)
if r.Text != nil && *r.Text != "" {
fmt.Fprintf(b, "Output:\n```\n%s\n```\n", strings.TrimRight(*r.Text, "\n"))
}
case api.Option:
if p.Item.Payload["multiple"] == true {
fmt.Fprintf(b, "%s checked:\n", r.User)
+5 -2
View File
@@ -16,6 +16,7 @@ import (
"g.fc.run/misc/byre/internal/api"
"g.fc.run/misc/byre/internal/claude"
"g.fc.run/misc/byre/internal/clients"
"g.fc.run/misc/byre/internal/git"
"g.fc.run/misc/byre/internal/mcp"
"g.fc.run/misc/byre/internal/store"
@@ -39,6 +40,7 @@ type Manager struct {
codexHome string
status func(context.Context) (api.Status, error)
addProject func(ctx context.Context, name, repoPath, createdBy string) (api.Project, error)
clients *clients.Registry
mu sync.Mutex
live map[string]*live
@@ -117,7 +119,7 @@ func (l *live) implementer() bool {
return l.owner == "" && l.reviewID == ""
}
func New(st *store.Store, worktreeRoot, leadRoot, codexHome string, status func(context.Context) (api.Status, error), addProject func(ctx context.Context, name, repoPath, createdBy string) (api.Project, error)) *Manager {
func New(st *store.Store, worktreeRoot, leadRoot, codexHome string, status func(context.Context) (api.Status, error), addProject func(ctx context.Context, name, repoPath, createdBy string) (api.Project, error), clientRegistry *clients.Registry) *Manager {
m := &Manager{
store: st,
worktreeRoot: worktreeRoot,
@@ -125,6 +127,7 @@ func New(st *store.Store, worktreeRoot, leadRoot, codexHome string, status func(
codexHome: codexHome,
status: status,
addProject: addProject,
clients: clientRegistry,
live: map[string]*live{},
starting: map[string]*start{},
deliverLocks: map[string]*sessionLock{},
@@ -709,7 +712,7 @@ func (m *Manager) startLive(ctx context.Context, sess api.AgentSession, task api
if err != nil {
return nil, err
}
tools := append([]mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.setMergeTitleTool(l), m.stopMergeTool(l), m.setTaskSummaryTool(l), m.searchClosedTool(l), m.exposePortTool(l), m.runOnClientTool(l), m.takeSnapshotTool(l)}, m.subtaskTools(l)...)
tools := append([]mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.setMergeTitleTool(l), m.stopMergeTool(l), m.setTaskSummaryTool(l), m.searchClosedTool(l), m.exposePortTool(l), m.takeSnapshotTool(l)}, append(m.subtaskTools(l), m.clientTools(l)...)...)
if sess.Role == api.Implementer {
g += longTitleGuidance(task)
notes, err := m.notesText(ctx, task.CreatedBy, task.ProjectId)
-4
View File
@@ -113,10 +113,6 @@ func CheckResponse(item api.InboxItem, c api.InboxResponseCreate) (api.InboxStat
return "", fmt.Errorf("%w: item %s: "+format, append([]any{ErrInvalid, item.Id}, args...)...)
}
if item.Kind == api.ClientRun && c.Type != api.Dismiss {
return bad("a run request is only dismissed here; runs are reported to /api/inbox/%s/runs", item.Id)
}
switch c.Type {
case api.Option:
if !question {
+359 -329
View File
@@ -135,6 +135,27 @@ func (e AttachmentKind) Valid() bool {
}
}
// Defines values for ClientRequestOp.
const (
Bash ClientRequestOp = "bash"
Copy ClientRequestOp = "copy"
Hands ClientRequestOp = "hands"
)
// Valid indicates whether the value is a known member of the ClientRequestOp enum.
func (e ClientRequestOp) Valid() bool {
switch e {
case Bash:
return true
case Copy:
return true
case Hands:
return true
default:
return false
}
}
// Defines values for DirEntryType.
const (
DirEntryTypeDir DirEntryType = "dir"
@@ -203,7 +224,6 @@ func (e FindingState) Valid() bool {
// Defines values for InboxItemKind.
const (
ClientRun InboxItemKind = "client_run"
Question InboxItemKind = "question"
RateLimited InboxItemKind = "rate_limited"
SessionError InboxItemKind = "session_error"
@@ -212,8 +232,6 @@ const (
// Valid indicates whether the value is a known member of the InboxItemKind enum.
func (e InboxItemKind) Valid() bool {
switch e {
case ClientRun:
return true
case Question:
return true
case RateLimited:
@@ -230,7 +248,6 @@ const (
Dismiss InboxResponseType = "dismiss"
Option InboxResponseType = "option"
Other InboxResponseType = "other"
Run InboxResponseType = "run"
)
// Valid indicates whether the value is a known member of the InboxResponseType enum.
@@ -242,8 +259,6 @@ func (e InboxResponseType) Valid() bool {
return true
case Other:
return true
case Run:
return true
default:
return false
}
@@ -637,6 +652,50 @@ type Blob struct {
Id string `json:"id"`
}
// ClientReply defines model for ClientReply.
type ClientReply struct {
// Error Set when the request couldn't be carried out.
Error *string `json:"error,omitempty"`
// ExitCode bash: the exit status, -1 when it was killed by its timeout.
ExitCode *int `json:"exit_code,omitempty"`
// Id The request's id.
Id string `json:"id"`
// Output bash: stdout and stderr together, at most their last 64 KiB.
Output *string `json:"output,omitempty"`
// Result hands: the tool's MCP CallToolResult.
Result *map[string]interface{} `json:"result,omitempty"`
}
// ClientRequest Work a task agent asks a connected Mac to do, sent over /api/clients/connect.
type ClientRequest struct {
// Arguments hands: the tool's arguments.
Arguments *map[string]interface{} `json:"arguments,omitempty"`
// Command bash: run with zsh as a login shell in the home directory, in its own process group.
Command *string `json:"command,omitempty"`
// Dest copy: where the archive's one entry ends up, absolute or starting with ~/; whatever is there is replaced.
Dest *string `json:"dest,omitempty"`
Id string `json:"id"`
Op ClientRequestOp `json:"op"`
// TimeoutMs bash: kill the process group after this long.
TimeoutMs *int `json:"timeout_ms,omitempty"`
// Tool hands: the byre-hands tool to call.
Tool *string `json:"tool,omitempty"`
// Transfer copy: the transfer id to download from /api/clients/transfers/{transferId}.
Transfer *string `json:"transfer,omitempty"`
}
// ClientRequestOp defines model for ClientRequest.Op.
type ClientRequestOp string
// ConnectedClient defines model for ConnectedClient.
type ConnectedClient struct {
// Commit The git commit the app was built from
@@ -771,7 +830,7 @@ type InboxItem struct {
Id string `json:"id"`
Kind InboxItemKind `json:"kind"`
// Payload question: context (Markdown with the choices inline as `- ( ) text` / `- (*) text` pick-one or `- [ ] text` / `- [x] text` pick-any list items), question, why_blocked, options (each choice's text, in order), multiple (true for pick-any), recommended (the marked choices' text), attachments. session_error: error. rate_limited: vendor, rate_limit (the vendor's rate-limit status), error. client_run: why, command, paths (worktree paths in the bundle), bytes (the bundle's size).
// Payload question: context (Markdown with the choices inline as `- ( ) text` / `- (*) text` pick-one or `- [ ] text` / `- [x] text` pick-any list items), question, why_blocked, options (each choice's text, in order), multiple (true for pick-any), recommended (the marked choices' text), attachments. session_error: error. rate_limited: vendor, rate_limit (the vendor's rate-limit status), error.
Payload map[string]interface{} `json:"payload"`
ProjectId string `json:"project_id"`
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
@@ -793,25 +852,17 @@ type InboxRespond struct {
// InboxResponse defines model for InboxResponse.
type InboxResponse struct {
Attachments []Attachment `json:"attachments"`
// Client run: the host name of the Mac that ran the command.
Client *string `json:"client,omitempty"`
CreatedAt time.Time `json:"created_at"`
DeliveredAt *time.Time `json:"delivered_at,omitempty"`
// ExitCode run: the command's exit status.
ExitCode *int `json:"exit_code,omitempty"`
Id string `json:"id"`
CreatedAt time.Time `json:"created_at"`
DeliveredAt *time.Time `json:"delivered_at,omitempty"`
Id string `json:"id"`
// Images Blob ids sent to the agent as images alongside the response.
Images []string `json:"images"`
ItemId string `json:"item_id"`
Options *[]string `json:"options,omitempty"`
// Text run: the command's output, at most its last 64 KiB.
Text *string `json:"text,omitempty"`
Type InboxResponseType `json:"type"`
User string `json:"user"`
Images []string `json:"images"`
ItemId string `json:"item_id"`
Options *[]string `json:"options,omitempty"`
Text *string `json:"text,omitempty"`
Type InboxResponseType `json:"type"`
User string `json:"user"`
}
// InboxResponseCreate defines model for InboxResponseCreate.
@@ -975,16 +1026,6 @@ type Review struct {
// ReviewState skipped: the snapshot's step changed no files, so no reviewer ran.
type ReviewState string
// RunReport defines model for RunReport.
type RunReport struct {
Client string `json:"client"`
ExitCode int `json:"exit_code"`
Output string `json:"output"`
// RunId The Mac's id for this one run, used as the response's id; reporting it again records nothing new.
RunId string `json:"run_id"`
}
// SessionCreate defines model for SessionCreate.
type SessionCreate struct {
// Images Blob ids from uploadBlob, sent to the agent as images alongside the prompt.
@@ -1441,9 +1482,6 @@ type GetTaskTerminalParams struct {
// RespondInboxJSONRequestBody defines body for RespondInbox for application/json ContentType.
type RespondInboxJSONRequestBody = InboxRespond
// ReportRunJSONRequestBody defines body for ReportRun for application/json ContentType.
type ReportRunJSONRequestBody = RunReport
// UpdateMeJSONRequestBody defines body for UpdateMe for application/json ContentType.
type UpdateMeJSONRequestBody = UserUpdate
@@ -1495,6 +1533,12 @@ type ServerInterface interface {
// (GET /api/blobs/{blobId})
GetBlob(w http.ResponseWriter, r *http.Request, blobId string)
// (GET /api/clients/connect)
ConnectClient(w http.ResponseWriter, r *http.Request)
// (GET /api/clients/transfers/{transferId})
GetClientTransfer(w http.ResponseWriter, r *http.Request, transferId string)
// (GET /api/events)
StreamEvents(w http.ResponseWriter, r *http.Request, params StreamEventsParams)
@@ -1507,12 +1551,6 @@ type ServerInterface interface {
// (GET /api/inbox/{itemId})
GetInboxItem(w http.ResponseWriter, r *http.Request, itemId string)
// (GET /api/inbox/{itemId}/bundle)
GetRunBundle(w http.ResponseWriter, r *http.Request, itemId string)
// (POST /api/inbox/{itemId}/runs)
ReportRun(w http.ResponseWriter, r *http.Request, itemId string)
// (GET /api/me)
GetMe(w http.ResponseWriter, r *http.Request)
@@ -1736,6 +1774,46 @@ func (siw *ServerInterfaceWrapper) GetBlob(w http.ResponseWriter, r *http.Reques
handler.ServeHTTP(w, r)
}
// ConnectClient operation middleware
func (siw *ServerInterfaceWrapper) ConnectClient(w http.ResponseWriter, r *http.Request) {
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.ConnectClient(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// GetClientTransfer operation middleware
func (siw *ServerInterfaceWrapper) GetClientTransfer(w http.ResponseWriter, r *http.Request) {
var err error
_ = err
// ------------- Path parameter "transferId" -------------
var transferId string
err = runtime.BindStyledParameterWithOptions("simple", "transferId", r.PathValue("transferId"), &transferId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "transferId", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetClientTransfer(w, r, transferId)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// StreamEvents operation middleware
func (siw *ServerInterfaceWrapper) StreamEvents(w http.ResponseWriter, r *http.Request) {
@@ -1915,58 +1993,6 @@ func (siw *ServerInterfaceWrapper) GetInboxItem(w http.ResponseWriter, r *http.R
handler.ServeHTTP(w, r)
}
// GetRunBundle operation middleware
func (siw *ServerInterfaceWrapper) GetRunBundle(w http.ResponseWriter, r *http.Request) {
var err error
_ = err
// ------------- Path parameter "itemId" -------------
var itemId string
err = runtime.BindStyledParameterWithOptions("simple", "itemId", r.PathValue("itemId"), &itemId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "itemId", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetRunBundle(w, r, itemId)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// ReportRun operation middleware
func (siw *ServerInterfaceWrapper) ReportRun(w http.ResponseWriter, r *http.Request) {
var err error
_ = err
// ------------- Path parameter "itemId" -------------
var itemId string
err = runtime.BindStyledParameterWithOptions("simple", "itemId", r.PathValue("itemId"), &itemId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "itemId", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.ReportRun(w, r, itemId)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// GetMe operation middleware
func (siw *ServerInterfaceWrapper) GetMe(w http.ResponseWriter, r *http.Request) {
@@ -3612,8 +3638,8 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/blobs/{blobId}", wrapper.GetBlob)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/inbox", wrapper.ListInbox)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/inbox/{itemId}", wrapper.GetInboxItem)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/inbox/{itemId}/bundle", wrapper.GetRunBundle)
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/inbox/{itemId}/runs", wrapper.ReportRun)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/clients/connect", wrapper.ConnectClient)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/clients/transfers/{transferId}", wrapper.GetClientTransfer)
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/inbox/responses", wrapper.RespondInbox)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/events", wrapper.StreamEvents)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/admin/status", wrapper.GetStatus)
@@ -3630,238 +3656,242 @@ 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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",
"IcrTpVneoBlbC8wO7vN7FZD59On/DgA=",
"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",
}
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
+202
View File
@@ -0,0 +1,202 @@
package clients
import (
"context"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"log/slog"
"slices"
"sort"
"strings"
"sync"
"time"
"github.com/coder/websocket"
"g.fc.run/misc/byre/internal/api"
)
const (
readLimit = 256 << 20
pingInterval = 30 * time.Second
pingTimeout = 15 * time.Second
)
type Client struct {
Login string
Host string
Commit string
ConnectedAt time.Time
}
type Transfer struct {
Dir string
Name string
}
type conn struct {
Client
ws *websocket.Conn
done chan struct{}
mu sync.Mutex
pending map[string]chan api.ClientReply
}
type Registry struct {
mu sync.Mutex
conns map[string]*conn
transfers map[string]Transfer
}
func New() *Registry {
return &Registry{conns: map[string]*conn{}, transfers: map[string]Transfer{}}
}
func (r *Registry) Serve(ctx context.Context, ws *websocket.Conn, client Client) {
ws.SetReadLimit(readLimit)
c := &conn{Client: client, 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
r.mu.Unlock()
if old != nil {
old.ws.Close(websocket.StatusGoingAway, "replaced by a newer connection from this host")
}
slog.Info("client connected", "host", client.Host, "login", client.Login, "commit", client.Commit)
defer func() {
r.mu.Lock()
if r.conns[client.Host] == c {
delete(r.conns, client.Host)
}
r.mu.Unlock()
close(c.done)
slog.Info("client disconnected", "host", client.Host, "login", client.Login)
}()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go c.ping(ctx)
for {
_, b, err := ws.Read(ctx)
if err != nil {
return
}
reply := api.ClientReply{}
err = json.Unmarshal(b, &reply)
if err != nil {
ws.Close(websocket.StatusUnsupportedData, "messages must be ClientReply")
return
}
c.mu.Lock()
ch := c.pending[reply.Id]
delete(c.pending, reply.Id)
c.mu.Unlock()
if ch == nil {
slog.Warn("client reply for no request", "host", client.Host, "id", reply.Id)
continue
}
ch <- reply
}
}
func (c *conn) ping(ctx context.Context) {
t := time.NewTicker(pingInterval)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
pctx, cancel := context.WithTimeout(ctx, pingTimeout)
err := c.ws.Ping(pctx)
cancel()
if err != nil {
c.ws.Close(websocket.StatusGoingAway, "ping timed out")
return
}
}
}
}
func (r *Registry) List() []Client {
r.mu.Lock()
defer r.mu.Unlock()
out := []Client{}
for _, c := range r.conns {
out = append(out, c.Client)
}
sort.Slice(out, func(i, j int) bool { return out[i].Host < out[j].Host })
return out
}
func (r *Registry) Call(ctx context.Context, host 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)
}
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, ", "))
}
req.Id = rand.Text()
ch := make(chan api.ClientReply, 1)
c.mu.Lock()
c.pending[req.Id] = ch
c.mu.Unlock()
defer func() {
c.mu.Lock()
delete(c.pending, req.Id)
c.mu.Unlock()
}()
data, err := json.Marshal(req)
if err != nil {
return api.ClientReply{}, err
}
err = c.ws.Write(ctx, websocket.MessageText, data)
if err != nil {
return api.ClientReply{}, fmt.Errorf("send to %s: %w", host, err)
}
select {
case reply := <-ch:
if reply.Error != nil {
return reply, errors.New(*reply.Error)
}
return reply, nil
case <-c.done:
return api.ClientReply{}, fmt.Errorf("%s disconnected before replying", host)
case <-ctx.Done():
return api.ClientReply{}, fmt.Errorf("%s didn't reply: %w", host, ctx.Err())
}
}
func (r *Registry) AddTransfer(t Transfer) string {
id := rand.Text()
r.mu.Lock()
defer r.mu.Unlock()
r.transfers[id] = t
return id
}
func (r *Registry) RemoveTransfer(id string) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.transfers, id)
}
func (r *Registry) Transfer(id string) (Transfer, bool) {
r.mu.Lock()
defer r.mu.Unlock()
t, ok := r.transfers[id]
return t, ok
}
+9 -4
View File
@@ -744,11 +744,16 @@ func (s *Session) callTool(raw json.RawMessage) (any, error) {
return nil, fmt.Errorf("unknown tool %s in %s", p.Tool, p.Namespace)
}
text, err := t.Handler(context.Background(), p.Arguments)
if err != nil {
return DynamicToolCallResponse{ContentItems: []DynamicToolCallOutputContentItem{{Type: DynamicToolCallOutputContentItemTypeInputText, Text: err.Error()}}}, nil
res := t.Call(context.Background(), p.Arguments)
items := []DynamicToolCallOutputContentItem{}
for _, c := range res.Content {
if c.Type == "image" {
items = append(items, DynamicToolCallOutputContentItem{Type: DynamicToolCallOutputContentItemTypeInputImage, ImageURL: "data:" + c.MimeType + ";base64," + c.Data})
continue
}
items = append(items, DynamicToolCallOutputContentItem{Type: DynamicToolCallOutputContentItemTypeInputText, Text: c.Text})
}
return DynamicToolCallResponse{Success: true, ContentItems: []DynamicToolCallOutputContentItem{{Type: DynamicToolCallOutputContentItemTypeInputText, Text: text}}}, nil
return DynamicToolCallResponse{Success: !res.IsError, ContentItems: items}, nil
}
func (s *Session) badLine(line []byte, reason string) {
+36 -16
View File
@@ -13,6 +13,41 @@ type Tool struct {
Description string
InputSchema json.RawMessage
Handler func(ctx context.Context, args json.RawMessage) (string, error)
Rich func(ctx context.Context, args json.RawMessage) Result
}
type Content struct {
Type string `json:"type"`
Text string `json:"text"`
Data string `json:"data"`
MimeType string `json:"mimeType"`
}
func (c Content) MarshalJSON() ([]byte, error) {
if c.Type == "image" {
return json.Marshal(map[string]string{"type": c.Type, "data": c.Data, "mimeType": c.MimeType})
}
return json.Marshal(map[string]string{"type": c.Type, "text": c.Text})
}
type Result struct {
Content []Content `json:"content"`
IsError bool `json:"isError,omitempty"`
}
func TextResult(text string, isError bool) Result {
return Result{Content: []Content{{Type: "text", Text: text}}, IsError: isError}
}
func (t Tool) Call(ctx context.Context, args json.RawMessage) Result {
if t.Rich != nil {
return t.Rich(ctx, args)
}
text, err := t.Handler(ctx, args)
if err != nil {
return TextResult(err.Error(), true)
}
return TextResult(text, false)
}
type Server struct {
@@ -52,16 +87,6 @@ type toolDesc struct {
InputSchema json.RawMessage `json:"inputSchema"`
}
type content struct {
Type string `json:"type"`
Text string `json:"text"`
}
type callResult struct {
Content []content `json:"content"`
IsError bool `json:"isError,omitempty"`
}
func (s *Server) Tool(name string) (Tool, bool) {
for _, t := range s.Tools {
if t.Name == name {
@@ -128,10 +153,5 @@ func (s *Server) call(ctx context.Context, params json.RawMessage) (any, *rpcErr
return nil, &rpcError{Code: -32602, Message: fmt.Sprintf("unknown tool: %s", p.Name)}
}
text, err := t.Handler(ctx, p.Arguments)
if err != nil {
return callResult{Content: []content{{Type: "text", Text: err.Error()}}, IsError: true}, nil
}
return callResult{Content: []content{{Type: "text", Text: text}}}, nil
return t.Call(ctx, p.Arguments), nil
}
+56
View File
@@ -0,0 +1,56 @@
package server
import (
"bytes"
"errors"
"fmt"
"log/slog"
"net/http"
"os/exec"
"time"
"github.com/coder/websocket"
"g.fc.run/misc/byre/internal/clients"
)
func (s *Server) ConnectClient(w http.ResponseWriter, r *http.Request) {
host := r.Header.Get("Byre-Host")
if host == "" {
writeError(w, http.StatusBadRequest, errors.New("missing Byre-Host"))
return
}
ws, err := websocket.Accept(w, r, nil)
if err != nil {
slog.Warn("accept client websocket", "host", host, "err", err)
return
}
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(),
})
}
func (s *Server) GetClientTransfer(w http.ResponseWriter, r *http.Request, transferID string) {
t, ok := s.clients.Transfer(transferID)
if !ok {
writeError(w, http.StatusNotFound, fmt.Errorf("no transfer %s", transferID))
return
}
w.Header().Set("Content-Type", "application/gzip")
cmd := exec.CommandContext(r.Context(), "tar", "-czf", "-", "-C", t.Dir, "--", t.Name)
cmd.Stdout = w
stderr := &bytes.Buffer{}
cmd.Stderr = stderr
err := cmd.Run()
if err != nil {
slog.Error("stream client transfer", "transfer", transferID, "dir", t.Dir, "name", t.Name, "err", err, "stderr", stderr.String())
panic(http.ErrAbortHandler)
}
}
-62
View File
@@ -1,12 +1,7 @@
package server
import (
"errors"
"fmt"
"io/fs"
"log/slog"
"net/http"
"os"
"g.fc.run/misc/byre/internal/api"
"g.fc.run/misc/byre/internal/store"
@@ -41,63 +36,6 @@ func (s *Server) GetInboxItem(w http.ResponseWriter, r *http.Request, itemID str
writeJSON(w, http.StatusOK, item)
}
func (s *Server) GetRunBundle(w http.ResponseWriter, r *http.Request, itemID string) {
item, err := s.store.GetInboxItem(r.Context(), itemID)
if err != nil {
writeStoreError(w, err)
return
}
if item.Kind != api.ClientRun {
writeError(w, http.StatusBadRequest, fmt.Errorf("item %s is a %s, not a client_run", itemID, item.Kind))
return
}
if item.State != api.Open {
writeError(w, http.StatusConflict, fmt.Errorf("run request %s is %s", itemID, item.State))
return
}
f, err := os.Open(s.store.BundlePath(itemID))
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
defer f.Close()
info, err := f.Stat()
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
w.Header().Set("Content-Type", "application/gzip")
http.ServeContent(w, r, "", info.ModTime(), f)
}
func (s *Server) ReportRun(w http.ResponseWriter, r *http.Request, itemID string) {
req := api.RunReport{}
if !readJSON(w, r, &req) {
return
}
item, err := s.agents.ReportRun(r.Context(), actor(r), itemID, req)
if err != nil {
writeAgentError(w, err)
return
}
writeJSON(w, http.StatusOK, item)
}
func (s *Server) removeClosedBundles(events []api.Event) {
for _, ev := range events {
if ev.Kind != "inbox.updated" || ev.Payload["kind"] != string(api.ClientRun) || ev.Payload["state"] == string(api.Open) {
continue
}
id, _ := ev.Payload["id"].(string)
err := os.Remove(s.store.BundlePath(id))
if err != nil && !errors.Is(err, fs.ErrNotExist) {
slog.Error("remove run bundle", "item", id, "err", err)
}
}
}
func (s *Server) RespondInbox(w http.ResponseWriter, r *http.Request) {
req := api.InboxRespond{}
if !readJSON(w, r, &req) {
+5 -8
View File
@@ -16,6 +16,7 @@ import (
"g.fc.run/misc/byre/internal/agents"
"g.fc.run/misc/byre/internal/api"
"g.fc.run/misc/byre/internal/clients"
"g.fc.run/misc/byre/internal/git"
"g.fc.run/misc/byre/internal/store"
"g.fc.run/misc/byre/internal/todo"
@@ -26,6 +27,7 @@ type Server struct {
store *store.Store
agents *agents.Manager
hub *hub
clients *clients.Registry
terminals *terminals
startedAt time.Time
@@ -43,6 +45,7 @@ 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{},
@@ -65,7 +68,7 @@ func New(dataDir string, portFirst int) (*Server, error) {
}
}
s.agents = agents.New(s.store, filepath.Join(dataDir, "worktrees"), filepath.Join(dataDir, "leads"), filepath.Join(dataDir, "codex"), s.status, s.createProject)
s.agents = agents.New(s.store, filepath.Join(dataDir, "worktrees"), filepath.Join(dataDir, "leads"), filepath.Join(dataDir, "codex"), s.status, s.createProject, s.clients)
err = s.agents.Recover(context.Background())
if err != nil {
s.store.Close()
@@ -81,7 +84,6 @@ func (s *Server) publish(events []api.Event) {
s.hub.publish(ev)
}
s.agents.Observe(events)
s.removeClosedBundles(events)
for _, ev := range events {
if (ev.Kind == "task.merged" || ev.Kind == "task.discarded") && ev.TaskId != nil {
s.terminals.close(*ev.TaskId)
@@ -134,7 +136,7 @@ func (s *Server) identify(next http.Handler) http.Handler {
return
}
if (r.Method != http.MethodGet && !automaticRequests[r.Pattern]) || humanGets[r.Pattern] {
if r.Method != http.MethodGet && !automaticRequests[r.Pattern] {
s.acted(login, r)
}
@@ -155,11 +157,6 @@ func (s *Server) clientsChanged() {
var automaticRequests = map[string]bool{
"PUT /api/me/pane-focus": true,
"PUT /api/tasks/{taskId}/summary-read": true,
"POST /api/inbox/{itemId}/runs": true,
}
var humanGets = map[string]bool{
"GET /api/inbox/{itemId}/bundle": true,
}
func (s *Server) ensureUser(ctx context.Context, login, name string) error {
+5 -40
View File
@@ -6,7 +6,6 @@ import (
"encoding/json"
"errors"
"fmt"
"path/filepath"
"strings"
"time"
@@ -15,7 +14,7 @@ import (
func DeliverableResponse(t api.InboxResponseType) bool {
switch t {
case api.Option, api.Other, api.Run:
case api.Option, api.Other:
return true
}
return false
@@ -168,31 +167,6 @@ func (s *Store) Respond(ctx context.Context, actor string, resolutions []Resolut
var ErrNotOpen = errors.New("inbox item is not open")
func (s *Store) AddRunResponse(ctx context.Context, resp api.InboxResponse) (bool, error) {
added := false
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
res, err := tx.ExecContext(ctx, `
INSERT INTO inbox_responses (id, item_id, user_login, type, text, client, exit_code, images, attachments, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, '[]', '[]', ?)
ON CONFLICT (id) DO NOTHING
`, resp.Id, resp.ItemId, resp.User, string(api.Run), resp.Text, *resp.Client, *resp.ExitCode, formatTime(resp.CreatedAt))
if err != nil {
return err
}
n, err := res.RowsAffected()
if err != nil || n == 0 {
return err
}
added = true
return emitItem(ctx, tx, emit, "inbox.updated", resp.ItemId, resp.User)
})
return added, err
}
func (s *Store) BundlePath(itemID string) string {
return filepath.Join(s.dataDir(), "bundles", itemID+".tar.gz")
}
func (s *Store) WithdrawItem(ctx context.Context, id string) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
res, err := tx.ExecContext(ctx, `UPDATE inbox_items SET state = ?, resolved_at = ? WHERE id = ? AND state = ?`,
@@ -261,9 +235,9 @@ type Pending struct {
func (s *Store) Undelivered(ctx context.Context, sessionID string) ([]Pending, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT r.id, r.item_id FROM inbox_responses r JOIN inbox_items i ON i.id = r.item_id
WHERE i.agent_session_id = ? AND r.delivered_at IS NULL AND r.type IN (?, ?, ?)
WHERE i.agent_session_id = ? AND r.delivered_at IS NULL AND r.type IN (?, ?)
ORDER BY r.created_at
`, sessionID, string(api.Option), string(api.Other), string(api.Run))
`, sessionID, string(api.Option), string(api.Other))
if err != nil {
return nil, err
}
@@ -373,7 +347,7 @@ func getItem(ctx context.Context, q querier, id string) (api.InboxItem, error) {
func getResponses(ctx context.Context, q querier, itemID string) ([]api.InboxResponse, error) {
rows, err := q.QueryContext(ctx, `
SELECT id, item_id, user_login, type, options, text, client, exit_code, images, attachments, created_at, delivered_at
SELECT id, item_id, user_login, type, options, text, images, attachments, created_at, delivered_at
FROM inbox_responses WHERE item_id = ? ORDER BY created_at
`, itemID)
if err != nil {
@@ -387,24 +361,15 @@ func getResponses(ctx context.Context, q querier, itemID string) ([]api.InboxRes
typ := ""
options := sql.NullString{}
text := sql.NullString{}
client := ""
exitCode := sql.NullInt64{}
images := ""
attachments := ""
createdAt := ""
delivered := sql.NullString{}
err = rows.Scan(&r.Id, &r.ItemId, &r.User, &typ, &options, &text, &client, &exitCode, &images, &attachments, &createdAt, &delivered)
err = rows.Scan(&r.Id, &r.ItemId, &r.User, &typ, &options, &text, &images, &attachments, &createdAt, &delivered)
if err != nil {
return nil, err
}
if client != "" {
r.Client = &client
}
if exitCode.Valid {
code := int(exitCode.Int64)
r.ExitCode = &code
}
r.Type = api.InboxResponseType(typ)
if options.Valid {
+5
View File
@@ -469,6 +469,11 @@ ALTER TABLE agent_sessions ADD COLUMN subagents INTEGER NOT NULL DEFAULT 0;
`}, {"add-inbox-run-responses", `
ALTER TABLE inbox_responses ADD COLUMN client TEXT NOT NULL DEFAULT '';
ALTER TABLE inbox_responses ADD COLUMN exit_code INTEGER;
`}, {"drop-client-runs", `
DELETE FROM inbox_responses WHERE item_id IN (SELECT id FROM inbox_items WHERE kind = 'client_run');
DELETE FROM inbox_items WHERE kind = 'client_run';
ALTER TABLE inbox_responses DROP COLUMN client;
ALTER TABLE inbox_responses DROP COLUMN exit_code;
`}}
const (