Simplify codebase per audit of 0139564
This commit is contained in:
@@ -21,7 +21,7 @@ func (m *Manager) MergeAfterReview(ctx context.Context, task api.Task, user api.
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
}
|
||||
if target == api.MergeTargetParent && (task.ParentTaskId == nil || task.WorkflowStage != nil) {
|
||||
if target == api.MergeTargetParent && !isSubtask(task) {
|
||||
return api.Task{}, fmt.Errorf("%w: task %s is not a subtask", ErrInvalid, task.Id)
|
||||
}
|
||||
if target == api.MergeTargetMain {
|
||||
|
||||
@@ -221,9 +221,13 @@ func (m *Manager) agyBeforeModel(l *live) hookcmd.Func {
|
||||
|
||||
func (m *Manager) agyBeforeTool(l *live) hookcmd.Func {
|
||||
return func(ctx context.Context, input json.RawMessage) (map[string]any, error) {
|
||||
if !m.heldBeforeTool(ctx, l, "") || !m.midTurnWaiting(ctx, l) {
|
||||
if !m.resumedFromPause(ctx, l, "") {
|
||||
return map[string]any{"decision": "allow"}, nil
|
||||
}
|
||||
return map[string]any{"decision": "deny", "reason": pausedCallNotice}, nil
|
||||
mt := m.deliverMidTurn(ctx, l)
|
||||
if mt.text == "" {
|
||||
return map[string]any{"decision": "allow"}, nil
|
||||
}
|
||||
return map[string]any{"decision": "deny", "reason": pausedCallNotice + "\n\n" + mt.text}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +278,7 @@ func (m *Manager) listClients() string {
|
||||
now := time.Now()
|
||||
b := &strings.Builder{}
|
||||
for i, r := range rows {
|
||||
fmt.Fprintf(b, "- %s (%s, %s, id %s): ", r.Host, r.Device, r.Login, r.ID)
|
||||
fmt.Fprintf(b, "- %s (%s, %s, id %s): ", r.Host, r.Device, r.Login, r.Id)
|
||||
if r.LastActionAt == nil {
|
||||
b.WriteString("no action since byre started")
|
||||
} else {
|
||||
|
||||
@@ -210,7 +210,7 @@ func (m *Manager) steerMidTurn(ctx context.Context, l *live, s *codex.Session) {
|
||||
|
||||
func (m *Manager) codexAfterTool(l *live) hookcmd.Func {
|
||||
return func(ctx context.Context, input json.RawMessage) (map[string]any, error) {
|
||||
if m.holdWhilePaused(ctx, l, "") && ctx.Err() == nil {
|
||||
if m.resumedFromPause(ctx, l, "") {
|
||||
m.steerMidTurn(context.WithoutCancel(ctx), l, l.sess.(codexProcess).Session)
|
||||
}
|
||||
return map[string]any{}, nil
|
||||
|
||||
@@ -109,7 +109,7 @@ func (m *Manager) taskGuidance(ctx context.Context, task api.Task) (string, erro
|
||||
g += fmt.Sprintf("\n- This task was started from the item %q in %s at the repo root. When the work is done, remove that item from %s in this worktree, so the removal lands in the same merge as the work.",
|
||||
*task.TodoItem, todo.File, todo.File)
|
||||
}
|
||||
if task.ParentTaskId != nil && task.WorkflowStage == nil {
|
||||
if isSubtask(task) {
|
||||
parent, err := m.store.GetTask(ctx, *task.ParentTaskId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -418,23 +418,6 @@ func (m *Manager) deliverMidTurn(ctx context.Context, l *live) midTurn {
|
||||
return mt
|
||||
}
|
||||
|
||||
func (m *Manager) midTurnWaiting(ctx context.Context, l *live) bool {
|
||||
inputs, err := m.store.TaskInputs(ctx, l.taskID, l.member)
|
||||
if err != nil {
|
||||
slog.Error("list queued messages", "session", l.id, "err", err)
|
||||
return false
|
||||
}
|
||||
if len(inputs) > 0 {
|
||||
return true
|
||||
}
|
||||
pending, err := m.store.Undelivered(ctx, l.id)
|
||||
if err != nil {
|
||||
slog.Error("list undelivered responses", "session", l.id, "err", err)
|
||||
return false
|
||||
}
|
||||
return len(pending) > 0
|
||||
}
|
||||
|
||||
func (m *Manager) restoreMidTurn(ctx context.Context, l *live, mt midTurn) {
|
||||
if len(mt.taskInputs) > 0 {
|
||||
err := m.store.RestoreTaskInputs(ctx, mt.taskInputs)
|
||||
|
||||
@@ -392,7 +392,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
||||
return "", err
|
||||
}
|
||||
case "parent":
|
||||
if task.ParentTaskId != nil {
|
||||
if isSubtask(task) {
|
||||
err = m.checkTaskNotStopped(ctx, *task.ParentTaskId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -878,7 +878,7 @@ func (m *Manager) listTasksText(ctx context.Context) (string, error) {
|
||||
if t.ForkedFrom != nil {
|
||||
fmt.Fprintf(b, "; %s", forkOrigin(t, *t.ForkedFrom))
|
||||
}
|
||||
if t.ParentTaskId != nil && t.WorkflowStage == nil {
|
||||
if isSubtask(t) {
|
||||
fmt.Fprintf(b, "; subtask of %s", *t.ParentTaskId)
|
||||
if t.MergingIntoParent {
|
||||
b.WriteString(", merging into it")
|
||||
|
||||
@@ -166,13 +166,13 @@ func (m *Manager) holdWhilePaused(ctx context.Context, l *live, toolUseID string
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) heldBeforeTool(ctx context.Context, l *live, toolUseID string) bool {
|
||||
func (m *Manager) resumedFromPause(ctx context.Context, l *live, toolUseID string) bool {
|
||||
return m.holdWhilePaused(ctx, l, toolUseID) && ctx.Err() == nil
|
||||
}
|
||||
|
||||
func (m *Manager) beforeTool(l *live) hookcmd.Func {
|
||||
return func(ctx context.Context, input json.RawMessage) (map[string]any, error) {
|
||||
if !m.heldBeforeTool(ctx, l, hookToolUseID(input)) {
|
||||
if !m.resumedFromPause(ctx, l, hookToolUseID(input)) {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
mt := m.deliverMidTurn(ctx, l)
|
||||
|
||||
@@ -271,7 +271,7 @@ func (m *Manager) reviewPrompt(ctx context.Context, task api.Task, project api.P
|
||||
}
|
||||
|
||||
func (m *Manager) reviewBase(ctx context.Context, task api.Task, project api.Project) (string, error) {
|
||||
if task.ParentTaskId == nil || task.WorkflowStage != nil {
|
||||
if !isSubtask(task) {
|
||||
return project.DefaultBranch, nil
|
||||
}
|
||||
parent, err := m.store.GetTask(ctx, *task.ParentTaskId)
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"time"
|
||||
|
||||
"g.fc.run/ai/byre/internal/api"
|
||||
"g.fc.run/ai/byre/internal/clients"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -17,7 +16,7 @@ const (
|
||||
|
||||
func (m *Manager) signer(ctx context.Context, user api.User) func(payload []byte) ([]byte, error) {
|
||||
return func(payload []byte) ([]byte, error) {
|
||||
mine := []clients.Client{}
|
||||
mine := []api.ConnectedClient{}
|
||||
for _, c := range m.clients.RecentMacs() {
|
||||
if c.Login == user.Login {
|
||||
mine = append(mine, c)
|
||||
@@ -33,7 +32,7 @@ func (m *Manager) signer(ctx context.Context, user api.User) func(payload []byte
|
||||
ms := int(signTimeout.Milliseconds())
|
||||
cctx, cancel := context.WithTimeout(ctx, signTimeout+replyGrace)
|
||||
defer cancel()
|
||||
reply, err := m.clients.Call(cctx, client.ID, api.ClientRequest{Op: api.Sign, Data: &payload, Ident: &ident, TimeoutMs: &ms})
|
||||
reply, err := m.clients.Call(cctx, client.Id, api.ClientRequest{Op: api.Sign, Data: &payload, Ident: &ident, TimeoutMs: &ms})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -274,8 +274,12 @@ func (m *Manager) messageTask(ctx context.Context, task api.Task, user api.User,
|
||||
return err
|
||||
}
|
||||
|
||||
func isSubtask(task api.Task) bool {
|
||||
return task.ParentTaskId != nil && task.WorkflowStage == nil
|
||||
}
|
||||
|
||||
func (m *Manager) MergeIntoParent(ctx context.Context, task api.Task, user api.User) (api.Task, error) {
|
||||
if task.ParentTaskId == nil || task.WorkflowStage != nil {
|
||||
if !isSubtask(task) {
|
||||
return api.Task{}, fmt.Errorf("%w: task %s is not a subtask", ErrInvalid, task.Id)
|
||||
}
|
||||
err := m.checkNoSubtasks(ctx, task)
|
||||
@@ -384,7 +388,7 @@ func (m *Manager) reportToParent(taskID, result string) {
|
||||
slog.Error("load subtask to report", "task", taskID, "err", err)
|
||||
return
|
||||
}
|
||||
if task.ParentTaskId == nil || task.WorkflowStage != nil || task.MergingIntoParent {
|
||||
if !isSubtask(task) || task.MergingIntoParent {
|
||||
return
|
||||
}
|
||||
parent, err := m.store.GetTask(ctx, *task.ParentTaskId)
|
||||
|
||||
@@ -27,7 +27,6 @@ func (m *Manager) loadUsage() {
|
||||
m.mu.Lock()
|
||||
m.usageCommands = cmds
|
||||
m.mu.Unlock()
|
||||
go m.readCodexRateLimit()
|
||||
for _, v := range []api.AgentSessionVendor{api.Claude, api.Codex, api.Antigravity} {
|
||||
go m.readUsage(v)
|
||||
}
|
||||
@@ -67,9 +66,6 @@ func (m *Manager) SetUsageCommands(ctx context.Context, actor string, c api.Usag
|
||||
|
||||
m.store.Broadcast(api.Event{Kind: "admin.health", Actor: "byre", Payload: map[string]any{}})
|
||||
for _, v := range changed {
|
||||
if v == api.Codex && cmds[v] == "" {
|
||||
go m.readCodexRateLimit()
|
||||
}
|
||||
go m.readUsage(v)
|
||||
}
|
||||
return nil
|
||||
@@ -84,7 +80,10 @@ func (m *Manager) readUsage(vendor api.AgentSessionVendor) {
|
||||
cmdline := m.usageCommands[vendor]
|
||||
m.mu.Unlock()
|
||||
if cmdline == "" {
|
||||
if vendor == api.Antigravity {
|
||||
switch vendor {
|
||||
case api.Codex:
|
||||
m.readCodexRateLimit()
|
||||
case api.Antigravity:
|
||||
m.readAgyRateLimit()
|
||||
}
|
||||
return
|
||||
|
||||
+29
-41
@@ -28,18 +28,6 @@ const (
|
||||
|
||||
var ErrUnavailable = errors.New("client unavailable")
|
||||
|
||||
type Client struct {
|
||||
ID string
|
||||
Login string
|
||||
Host string
|
||||
Commit string
|
||||
Build string
|
||||
Device string
|
||||
ConnectedAt time.Time
|
||||
LastActionAt *time.Time
|
||||
Sync *api.ClientSync
|
||||
}
|
||||
|
||||
type Transfer struct {
|
||||
Dir string
|
||||
Name string
|
||||
@@ -54,7 +42,7 @@ type conn struct {
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
Client
|
||||
api.ConnectedClient
|
||||
conn *conn
|
||||
seen time.Time
|
||||
notified time.Time
|
||||
@@ -71,33 +59,34 @@ func New(changed func()) *Registry {
|
||||
return &Registry{clients: map[string]*entry{}, transfers: map[string]Transfer{}, changed: changed}
|
||||
}
|
||||
|
||||
func (r *Registry) entry(id, login, host, commit, build string, now time.Time) *entry {
|
||||
func (r *Registry) entry(from api.ConnectedClient, 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]
|
||||
e := r.clients[from.Id]
|
||||
if e == nil {
|
||||
e = &entry{Client: Client{ID: id}}
|
||||
r.clients[id] = e
|
||||
e = &entry{ConnectedClient: api.ConnectedClient{Id: from.Id}}
|
||||
r.clients[from.Id] = e
|
||||
}
|
||||
e.Login = login
|
||||
e.Host = host
|
||||
e.Commit = commit
|
||||
e.Build = build
|
||||
e.Login = from.Login
|
||||
e.Host = from.Host
|
||||
e.Device = from.Device
|
||||
e.Commit = from.Commit
|
||||
e.Build = from.Build
|
||||
e.seen = now
|
||||
return e
|
||||
}
|
||||
|
||||
func (r *Registry) Acted(id, login, host, commit, build string) bool {
|
||||
if id == "" {
|
||||
func (r *Registry) Acted(from api.ConnectedClient) bool {
|
||||
if from.Id == "" {
|
||||
return false
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
now := time.Now()
|
||||
e := r.entry(id, login, host, commit, build, now)
|
||||
e := r.entry(from, now)
|
||||
e.LastActionAt = &now
|
||||
if now.Sub(e.notified) < time.Second {
|
||||
return false
|
||||
@@ -106,28 +95,27 @@ func (r *Registry) Acted(id, login, host, commit, build string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Registry) Synced(id, login, host, commit, build string, sync api.ClientSync) {
|
||||
func (r *Registry) Synced(from api.ConnectedClient, sync api.ClientSync) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.entry(id, login, host, commit, build, time.Now()).Sync = &sync
|
||||
r.entry(from, time.Now()).Sync = &sync
|
||||
}
|
||||
|
||||
func (r *Registry) Serve(ctx context.Context, ws *websocket.Conn, id, login, host, commit, build, device string) {
|
||||
func (r *Registry) Serve(ctx context.Context, ws *websocket.Conn, from api.ConnectedClient) {
|
||||
ws.SetReadLimit(readLimit)
|
||||
c := &conn{ws: ws, done: make(chan struct{}), pending: map[string]chan api.ClientReply{}}
|
||||
|
||||
r.mu.Lock()
|
||||
now := time.Now().UTC()
|
||||
e := r.entry(id, login, host, commit, build, now)
|
||||
e := r.entry(from, now)
|
||||
old := e.conn
|
||||
e.conn = c
|
||||
e.Device = device
|
||||
e.ConnectedAt = now
|
||||
r.mu.Unlock()
|
||||
if old != nil {
|
||||
old.ws.Close(websocket.StatusGoingAway, "replaced by a newer connection from this client")
|
||||
}
|
||||
slog.Info("client connected", "id", id, "host", host, "device", device, "login", login, "build", build, "commit", commit)
|
||||
slog.Info("client connected", "id", from.Id, "host", from.Host, "device", from.Device, "login", from.Login, "build", from.Build, "commit", from.Commit)
|
||||
r.changed()
|
||||
|
||||
defer func() {
|
||||
@@ -138,7 +126,7 @@ func (r *Registry) Serve(ctx context.Context, ws *websocket.Conn, id, login, hos
|
||||
}
|
||||
r.mu.Unlock()
|
||||
close(c.done)
|
||||
slog.Info("client disconnected", "id", id, "host", host, "login", login)
|
||||
slog.Info("client disconnected", "id", from.Id, "host", from.Host, "login", from.Login)
|
||||
r.changed()
|
||||
}()
|
||||
|
||||
@@ -165,7 +153,7 @@ func (r *Registry) Serve(ctx context.Context, ws *websocket.Conn, id, login, hos
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if ch == nil {
|
||||
slog.Warn("client reply for no request", "host", host, "id", reply.Id)
|
||||
slog.Warn("client reply for no request", "host", from.Host, "id", reply.Id)
|
||||
continue
|
||||
}
|
||||
ch <- reply
|
||||
@@ -201,20 +189,20 @@ func (r *Registry) Host(id string) string {
|
||||
return e.Host
|
||||
}
|
||||
|
||||
func (r *Registry) List() []Client {
|
||||
func (r *Registry) List() []api.ConnectedClient {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
out := []Client{}
|
||||
out := []api.ConnectedClient{}
|
||||
for _, e := range r.clients {
|
||||
if e.conn != nil {
|
||||
out = append(out, e.Client)
|
||||
out = append(out, e.ConnectedClient)
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].ConnectedAt.Before(out[j].ConnectedAt) })
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Registry) Recent() []Client {
|
||||
func (r *Registry) Recent() []api.ConnectedClient {
|
||||
rows := r.List()
|
||||
sort.SliceStable(rows, func(i, j int) bool {
|
||||
a, b := rows[i].LastActionAt, rows[j].LastActionAt
|
||||
@@ -223,8 +211,8 @@ func (r *Registry) Recent() []Client {
|
||||
return rows
|
||||
}
|
||||
|
||||
func (r *Registry) RecentMacs() []Client {
|
||||
out := []Client{}
|
||||
func (r *Registry) RecentMacs() []api.ConnectedClient {
|
||||
out := []api.ConnectedClient{}
|
||||
for _, c := range r.Recent() {
|
||||
if c.Device == "Mac" {
|
||||
out = append(out, c)
|
||||
@@ -241,8 +229,8 @@ func (r *Registry) Call(ctx context.Context, name string, req api.ClientRequest)
|
||||
if e.conn == nil {
|
||||
continue
|
||||
}
|
||||
connected = append(connected, fmt.Sprintf("%s (%s)", e.Host, e.ID))
|
||||
if e.ID == name || e.Host == name {
|
||||
connected = append(connected, fmt.Sprintf("%s (%s)", e.Host, e.Id))
|
||||
if e.Id == name || e.Host == name {
|
||||
matches = append(matches, *e)
|
||||
}
|
||||
}
|
||||
@@ -254,7 +242,7 @@ func (r *Registry) Call(ctx context.Context, name string, req api.ClientRequest)
|
||||
if len(matches) > 1 {
|
||||
matched := make([]string, len(matches))
|
||||
for i, e := range matches {
|
||||
matched[i] = e.ID
|
||||
matched[i] = e.Id
|
||||
}
|
||||
slices.Sort(matched)
|
||||
return api.ClientReply{}, fmt.Errorf("%d clients on %s are connected; name one by its id: %s", len(matches), name, strings.Join(matched, ", "))
|
||||
|
||||
@@ -21,27 +21,25 @@ import (
|
||||
)
|
||||
|
||||
func (s *Server) ConnectClient(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.Header.Get("Byre-Client")
|
||||
host := clientHost(r)
|
||||
if id == "" || host == "" {
|
||||
c := requestClient(r, actor(r))
|
||||
if c.Id == "" || c.Host == "" {
|
||||
writeError(w, http.StatusBadRequest, errors.New("missing Byre-Client or Byre-Host, or Byre-Host isn't percent-encoded"))
|
||||
return
|
||||
}
|
||||
|
||||
ws, err := websocket.Accept(w, r, nil)
|
||||
if err != nil {
|
||||
slog.Warn("accept client websocket", "host", host, "err", err)
|
||||
slog.Warn("accept client websocket", "host", c.Host, "err", err)
|
||||
return
|
||||
}
|
||||
defer ws.CloseNow()
|
||||
|
||||
s.clients.Serve(r.Context(), ws, id, actor(r), host, r.Header.Get("Byre-Commit"), r.Header.Get("Byre-Build"), r.Header.Get("Byre-Device"))
|
||||
s.clients.Serve(r.Context(), ws, c)
|
||||
}
|
||||
|
||||
func (s *Server) SetClientSync(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.Header.Get("Byre-Client")
|
||||
host := clientHost(r)
|
||||
if id == "" || host == "" {
|
||||
c := requestClient(r, actor(r))
|
||||
if c.Id == "" || c.Host == "" {
|
||||
writeError(w, http.StatusBadRequest, errors.New("missing Byre-Client or Byre-Host, or Byre-Host isn't percent-encoded"))
|
||||
return
|
||||
}
|
||||
@@ -56,9 +54,9 @@ func (s *Server) SetClientSync(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, errors.New("a failed sync needs an error"))
|
||||
return
|
||||
}
|
||||
slog.Warn("client icloud sync failing", "id", id, "host", host, "error", *sync.Error)
|
||||
slog.Warn("client icloud sync failing", "id", c.Id, "host", c.Host, "error", *sync.Error)
|
||||
}
|
||||
s.clients.Synced(id, actor(r), host, r.Header.Get("Byre-Commit"), r.Header.Get("Byre-Build"), sync)
|
||||
s.clients.Synced(c, sync)
|
||||
s.clientsChanged()
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
@@ -86,7 +84,7 @@ func (s *Server) ExecClient(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusServiceUnavailable, errors.New("no client Macs are connected"))
|
||||
return
|
||||
}
|
||||
name = recent[0].ID
|
||||
name = recent[0].Id
|
||||
}
|
||||
|
||||
timeout := time.Duration(req.TimeoutMs) * time.Millisecond
|
||||
|
||||
+13
-20
@@ -203,11 +203,22 @@ func (s *Server) identify(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
func (s *Server) acted(login string, r *http.Request) {
|
||||
if s.clients.Acted(r.Header.Get("Byre-Client"), login, clientHost(r), r.Header.Get("Byre-Commit"), r.Header.Get("Byre-Build")) {
|
||||
if s.clients.Acted(requestClient(r, login)) {
|
||||
s.clientsChanged()
|
||||
}
|
||||
}
|
||||
|
||||
func requestClient(r *http.Request, login string) api.ConnectedClient {
|
||||
return api.ConnectedClient{
|
||||
Id: r.Header.Get("Byre-Client"),
|
||||
Login: login,
|
||||
Host: clientHost(r),
|
||||
Device: r.Header.Get("Byre-Device"),
|
||||
Commit: r.Header.Get("Byre-Commit"),
|
||||
Build: r.Header.Get("Byre-Build"),
|
||||
}
|
||||
}
|
||||
|
||||
func clientHost(r *http.Request) string {
|
||||
host, err := url.PathUnescape(r.Header.Get("Byre-Host"))
|
||||
if err != nil {
|
||||
@@ -216,24 +227,6 @@ func clientHost(r *http.Request) string {
|
||||
return host
|
||||
}
|
||||
|
||||
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,
|
||||
Device: c.Device,
|
||||
Commit: c.Commit,
|
||||
Build: c.Build,
|
||||
ConnectedAt: c.ConnectedAt,
|
||||
LastActionAt: c.LastActionAt,
|
||||
Sync: c.Sync,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Server) clientsChanged() {
|
||||
s.store.Broadcast(api.Event{Kind: "admin.clients", Actor: "byre", Payload: map[string]any{}})
|
||||
}
|
||||
@@ -720,7 +713,7 @@ func (s *Server) status(ctx context.Context) (api.Status, error) {
|
||||
Projects: c.Projects,
|
||||
ActiveTasks: c.ActiveTasks,
|
||||
SseClients: s.hub.count(),
|
||||
Clients: s.connectedClients(),
|
||||
Clients: s.clients.List(),
|
||||
LiveAgents: s.agents.LiveCount(),
|
||||
RunningTurns: usage.Running,
|
||||
RateLimits: usage.RateLimits,
|
||||
|
||||
+12
-48
@@ -629,9 +629,7 @@ func (s *Store) ParentMergeBy(ctx context.Context, id string) (string, error) {
|
||||
func (s *Store) ReserveMergeTurn(ctx context.Context, id, sessionID string) (bool, error) {
|
||||
reserved := false
|
||||
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
res, err := tx.ExecContext(ctx, `UPDATE tasks SET merge_state = 'turn' WHERE id = ? AND merge_state = 'claimed'
|
||||
AND NOT EXISTS (SELECT 1 FROM agent_sessions s WHERE s.task_id = tasks.id AND s.role = ? AND s.state = ?)`,
|
||||
id, string(api.Implementer), string(api.AgentSessionStateRunning))
|
||||
res, err := tx.ExecContext(ctx, `UPDATE tasks SET merge_state = 'turn' WHERE id = ? AND merge_state = 'claimed' AND NOT `+implementerRunning, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1339,28 +1337,18 @@ func (s *Store) PausedSessions(ctx context.Context) ([]api.AgentSession, error)
|
||||
}
|
||||
|
||||
func (s *Store) SetSessionPaused(ctx context.Context, id string, paused bool, actor string) error {
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
taskID := ""
|
||||
projectID := ""
|
||||
owner := ""
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT coalesce(t.id, ''), coalesce(t.project_id, ''), s.owner FROM agent_sessions s LEFT JOIN tasks t ON t.id = s.task_id WHERE s.id = ?
|
||||
`, id).Scan(&taskID, &projectID, &owner)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `UPDATE agent_sessions SET paused = ? WHERE id = ?`, paused, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return emit(sessionEvent("session.paused", projectID, taskID, owner, id, actor, map[string]any{"paused": paused}))
|
||||
})
|
||||
return s.updateSession(ctx, id, actor, "session.paused", map[string]any{"paused": paused}, `paused = ?`, paused)
|
||||
}
|
||||
|
||||
func (s *Store) SetSubagents(ctx context.Context, id string, n int, actor string) error {
|
||||
return s.updateSession(ctx, id, actor, "session.subagents", map[string]any{"subagents": n}, `subagents = ?`, n)
|
||||
}
|
||||
|
||||
func (s *Store) SetSessionModel(ctx context.Context, id, model, effort, actor string) error {
|
||||
return s.updateSession(ctx, id, actor, "session.model", map[string]any{"model": model, "effort": effort}, `model = ?, effort = ?`, model, effort)
|
||||
}
|
||||
|
||||
func (s *Store) updateSession(ctx context.Context, id, actor, kind string, payload map[string]any, set string, args ...any) error {
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
taskID := ""
|
||||
projectID := ""
|
||||
@@ -1374,11 +1362,11 @@ func (s *Store) SetSubagents(ctx context.Context, id string, n int, actor string
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `UPDATE agent_sessions SET subagents = ? WHERE id = ?`, n, id)
|
||||
_, err = tx.ExecContext(ctx, `UPDATE agent_sessions SET `+set+` WHERE id = ?`, append(args, id)...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return emit(sessionEvent("session.subagents", projectID, taskID, owner, id, actor, map[string]any{"subagents": n}))
|
||||
return emit(sessionEvent(kind, projectID, taskID, owner, id, actor, payload))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1465,30 +1453,6 @@ func (s *Store) SetVendorSessionID(ctx context.Context, id, vendorSessionID stri
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) SetSessionModel(ctx context.Context, id, model, effort, actor string) error {
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
taskID := ""
|
||||
projectID := ""
|
||||
owner := ""
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT coalesce(t.id, ''), coalesce(t.project_id, ''), s.owner FROM agent_sessions s LEFT JOIN tasks t ON t.id = s.task_id WHERE s.id = ?
|
||||
`, id).Scan(&taskID, &projectID, &owner)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, `UPDATE agent_sessions SET model = ?, effort = ? WHERE id = ?`, model, effort, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return emit(sessionEvent("session.model", projectID, taskID, owner, id, actor, map[string]any{"model": model, "effort": effort}))
|
||||
})
|
||||
}
|
||||
|
||||
type Counts struct {
|
||||
LastEventSeq int64
|
||||
Projects int
|
||||
|
||||
@@ -25,6 +25,10 @@ extension ByreTask {
|
||||
var name: String {
|
||||
title.isEmpty ? "New task" : title
|
||||
}
|
||||
|
||||
var isSubtask: Bool {
|
||||
parentTaskId != nil && workflowStage == nil
|
||||
}
|
||||
}
|
||||
|
||||
extension ByreEvent {
|
||||
|
||||
@@ -104,10 +104,7 @@ final class ChatModel {
|
||||
}
|
||||
|
||||
var hasUnmerged: Bool {
|
||||
guard let f = taskId.flatMap(store.findTask)?.mergeFacts else {
|
||||
return false
|
||||
}
|
||||
return f.unmergedCommits > 0 || f.unmergedFiles > 0
|
||||
taskId.flatMap(store.findTask)?.mergeFacts.hasChanges ?? false
|
||||
}
|
||||
|
||||
private var feed: Feed? {
|
||||
@@ -744,7 +741,7 @@ private struct MergeButton: View {
|
||||
? "Waiting for the merge in progress in this project to finish"
|
||||
: task.mergeAfterReview
|
||||
? "Merges when the running review finishes with no findings; open to merge now instead (⌘S)"
|
||||
: task.parentTaskId != nil && task.workflowStage == nil
|
||||
: task.isSubtask
|
||||
? "Merge this subtask into its parent task's branch, or into \(target) (⌘S)"
|
||||
: "The agent merges \(target) in, then byre squashes into one commit on \(target) and pushes it to origin; held while another task in the project is merging (⌘S)"
|
||||
Button(intoParent ? "Merging into parent" : held ? "Merge held" : task.mergeAfterReview ? "Merge after review" : keepOpen == false ? "Merge & complete" : "Merge") {
|
||||
|
||||
@@ -83,18 +83,20 @@ var modelChoices: [String: String] = [:]
|
||||
@MainActor
|
||||
var effortChoices: [String: String] = [:]
|
||||
|
||||
func vendorOf(_ model: String) -> String {
|
||||
typealias Vendor = Components.Schemas.AgentSession.VendorPayload
|
||||
|
||||
func vendorOf(_ model: String) -> Vendor {
|
||||
if model.hasPrefix("gpt-") {
|
||||
return "codex"
|
||||
return .codex
|
||||
}
|
||||
if model.hasPrefix("gemini-") {
|
||||
return "antigravity"
|
||||
return .antigravity
|
||||
}
|
||||
return "claude"
|
||||
return .claude
|
||||
}
|
||||
|
||||
func efforts(_ model: String) -> [String] {
|
||||
vendorOf(model) == "codex" ? ["", "low", "medium", "high", "xhigh"] : ["", "low", "medium", "high", "xhigh", "max"]
|
||||
vendorOf(model) == .codex ? ["", "low", "medium", "high", "xhigh"] : ["", "low", "medium", "high", "xhigh", "max"]
|
||||
}
|
||||
|
||||
func effortLabel(_ effort: String) -> String {
|
||||
@@ -755,7 +757,7 @@ struct ComposerView: View {
|
||||
let live = chat.closed ? chat.session : chat.liveSession
|
||||
let current = live.map { $0.model.isEmpty ? chat.builder.context[$0.id]?.model ?? "" : $0.model } ?? ""
|
||||
let currentEffort = live?.effort ?? ""
|
||||
let offered = live.map { s in models.filter { vendorOf($0) == s.vendor.rawValue } } ?? models
|
||||
let offered = live.map { s in models.filter { vendorOf($0) == s.vendor } } ?? models
|
||||
let options = !current.isEmpty && !offered.contains(current) ? [current] + offered : offered
|
||||
let modelKey = "message:\(live?.id ?? key)"
|
||||
let fresh = live == nil ? store.defaultModel : options[0]
|
||||
|
||||
@@ -4,6 +4,12 @@ import SwiftUI
|
||||
typealias Participant = Components.Schemas.Participant
|
||||
typealias MergeFacts = Components.Schemas.MergeFacts
|
||||
|
||||
extension MergeFacts {
|
||||
var hasChanges: Bool {
|
||||
unmergedCommits > 0 || unmergedFiles > 0
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private var mergeDrafts: [String: String] = [:]
|
||||
|
||||
@@ -67,11 +73,11 @@ func confirmMerge(_ taskId: String, _ store: Store, _ overlays: Overlays, keepOp
|
||||
return
|
||||
}
|
||||
let target = store.projects.first { $0.id == task.projectId }?.defaultBranch ?? "the default branch"
|
||||
if task.mergeFacts.unmergedCommits == 0 && task.mergeFacts.unmergedFiles == 0 {
|
||||
if !task.mergeFacts.hasChanges {
|
||||
overlays.showAlert("Nothing to merge: no commits or uncommitted files differ from \(target)")
|
||||
return
|
||||
}
|
||||
let parent = task.workflowStage == nil ? task.parentTaskId.flatMap { store.findTask($0) } : nil
|
||||
let parent = task.isSubtask ? task.parentTaskId.flatMap { store.findTask($0) } : nil
|
||||
let title = mergeDrafts[taskId] ?? task.mergeTitle
|
||||
mergeDrafts[taskId] = nil
|
||||
let model = MergeModel(taskId: taskId, target: target, keepOpen: keepOpen, parentTitle: !keepOpen && parent?.status == .active ? parent?.name : nil, title: title)
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
import ByreAPI
|
||||
import SwiftUI
|
||||
|
||||
let tabLabels: [TaskTab: String] = [
|
||||
.transcript: "Transcript",
|
||||
.board: "Board",
|
||||
.changes: "Changes",
|
||||
.files: "Files",
|
||||
.page: "Page",
|
||||
.captures: "Captures",
|
||||
.terminal: "Terminal",
|
||||
.workflow: "Workflow",
|
||||
]
|
||||
|
||||
#if os(macOS)
|
||||
struct HalfSplit: Layout {
|
||||
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
||||
@@ -362,7 +351,7 @@ struct TabStrip: View {
|
||||
}
|
||||
let run = store.findTask(taskId)?.workflow != nil
|
||||
let team = !store.team(taskId).isEmpty
|
||||
let order = taskTabs.filter { t in
|
||||
let order = TaskTab.allCases.filter { t in
|
||||
if run {
|
||||
return t == .workflow
|
||||
}
|
||||
@@ -382,7 +371,7 @@ struct TabStrip: View {
|
||||
|
||||
var body: some View {
|
||||
let info = info()
|
||||
FoldingTabs(tabs: info.order.map { ($0, tabLabels[$0]!) }, active: info.tab) { t in
|
||||
FoldingTabs(tabs: info.order.map { ($0, $0.label) }, active: info.tab) { t in
|
||||
store.openTab(t)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
|
||||
@@ -125,7 +125,7 @@ private struct SheetPanel: View {
|
||||
|
||||
private func title(_ task: ByreTask?) -> String {
|
||||
let label = switch sheet.view {
|
||||
case .tab(let t): tabLabels[t]!
|
||||
case .tab(let t): t.label
|
||||
case .image: "Image"
|
||||
case .diagram: "Diagram"
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ import Foundation
|
||||
import Observation
|
||||
import SwiftData
|
||||
|
||||
enum TaskTab: String {
|
||||
enum TaskTab: String, CaseIterable {
|
||||
case transcript, board, changes, files, page, captures, terminal, workflow
|
||||
}
|
||||
|
||||
let taskTabs: [TaskTab] = [.transcript, .board, .changes, .files, .page, .captures, .terminal, .workflow]
|
||||
var label: String {
|
||||
rawValue.capitalized
|
||||
}
|
||||
}
|
||||
|
||||
enum JumpTarget: Equatable {
|
||||
case question(String)
|
||||
@@ -614,7 +616,7 @@ final class Store {
|
||||
let kind = m?[1] ?? nil
|
||||
let id = m?[2] ?? nil
|
||||
let name = m?[3] ?? nil
|
||||
let tab = taskTabs.first { $0.rawValue == name }
|
||||
let tab = name.flatMap(TaskTab.init(rawValue:))
|
||||
if kind == "task", let id {
|
||||
reveals += 1
|
||||
if let tab, tab != .transcript, tab != .workflow {
|
||||
|
||||
@@ -38,7 +38,7 @@ func runGo(_ key: String, _ store: Store, _ overlays: Overlays) {
|
||||
break
|
||||
}
|
||||
Task {
|
||||
if let task = store.findTask(taskId), task.status == .active, task.state == .discardSuggested || (task.mergeFacts.unmergedCommits == 0 && task.mergeFacts.unmergedFiles == 0) {
|
||||
if let task = store.findTask(taskId), task.status == .active, task.state == .discardSuggested || !task.mergeFacts.hasChanges {
|
||||
await confirmDiscard(task, store, overlays)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user