Add team tasks: several agents in one worktree sharing a board
This commit is contained in:
@@ -125,9 +125,11 @@ func (s *Store) EndTurn(ctx context.Context, sessionID, actor string, payload ma
|
||||
role := ""
|
||||
state := DiscardNone
|
||||
discardSession := ""
|
||||
lead := false
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT coalesce(t.id, ''), coalesce(t.project_id, ''), s.owner, s.role, coalesce(t.discard_state, ''), coalesce(t.discard_session, '')
|
||||
FROM agent_sessions s LEFT JOIN tasks t ON t.id = s.task_id WHERE s.id = ?`, sessionID).Scan(&taskID, &projectID, &owner, &role, &state, &discardSession)
|
||||
SELECT coalesce(t.id, ''), coalesce(t.project_id, ''), s.owner, s.role, coalesce(t.discard_state, ''), coalesce(t.discard_session, ''),
|
||||
s.member = coalesce((SELECT f.member FROM agent_sessions f WHERE f.task_id = s.task_id AND f.role = s.role ORDER BY f.created_at LIMIT 1), '')
|
||||
FROM agent_sessions s LEFT JOIN tasks t ON t.id = s.task_id WHERE s.id = ?`, sessionID).Scan(&taskID, &projectID, &owner, &role, &state, &discardSession, &lead)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ErrNotFound
|
||||
}
|
||||
@@ -169,7 +171,7 @@ func (s *Store) EndTurn(ctx context.Context, sessionID, actor string, payload ma
|
||||
case state == DiscardRunning && discardSession == sessionID:
|
||||
next = DiscardClosing
|
||||
_, err = tx.ExecContext(ctx, `UPDATE tasks SET discard_state = ? WHERE id = ?`, string(DiscardClosing), taskID)
|
||||
case state == DiscardNone && finalizeMerge:
|
||||
case state == DiscardNone && finalizeMerge && lead:
|
||||
err = tx.QueryRowContext(ctx, `UPDATE tasks SET finalizing = merging_by, merging_by = '' WHERE id = ? AND merging_by != '' RETURNING finalizing`, taskID).Scan(&finalizing)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
|
||||
+21
-7
@@ -476,6 +476,9 @@ ALTER TABLE inbox_responses DROP COLUMN client;
|
||||
ALTER TABLE inbox_responses DROP COLUMN exit_code;
|
||||
`}, {"add-user-default-model", `
|
||||
ALTER TABLE users ADD COLUMN default_model TEXT NOT NULL DEFAULT 'claude-opus-5-5';
|
||||
`}, {"add-team-members", `
|
||||
ALTER TABLE agent_sessions ADD COLUMN member TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE task_inputs ADD COLUMN member TEXT NOT NULL DEFAULT '';
|
||||
`}}
|
||||
|
||||
const (
|
||||
@@ -1305,7 +1308,7 @@ func (s *Store) Participants(ctx context.Context, taskID string) ([]api.Particip
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT u.login, u.display_name, u.git_name, u.git_email
|
||||
FROM events e JOIN users u ON u.login = e.actor
|
||||
WHERE e.task_id = ? AND (e.kind = 'user.message' OR (e.kind = 'task.created' AND json_extract(e.payload, '$.forked_from') IS NOT NULL))
|
||||
WHERE e.task_id = ? AND (e.kind IN ('user.message', 'board.posted') OR (e.kind = 'task.created' AND json_extract(e.payload, '$.forked_from') IS NOT NULL))
|
||||
GROUP BY u.login ORDER BY min(e.seq)
|
||||
`, taskID)
|
||||
if err != nil {
|
||||
@@ -1349,6 +1352,17 @@ func (s *Store) TaskEvents(ctx context.Context, taskID string, after int64, limi
|
||||
return scanEvents(rows)
|
||||
}
|
||||
|
||||
func (s *Store) BoardPosts(ctx context.Context, taskID string, after int64) ([]api.Event, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT `+eventColumns+`
|
||||
FROM events WHERE task_id = ? AND kind = 'board.posted' AND seq > ? ORDER BY seq
|
||||
`, taskID, after)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return scanEvents(rows)
|
||||
}
|
||||
|
||||
func (s *Store) TaskEventsTail(ctx context.Context, taskID string, limit int) ([]api.Event, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT * FROM (SELECT `+eventColumns+` FROM events WHERE task_id = ? ORDER BY seq DESC LIMIT ?) ORDER BY seq
|
||||
@@ -1478,9 +1492,9 @@ func (s *Store) CreateSession(ctx context.Context, sess api.AgentSession, projec
|
||||
}
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO agent_sessions (id, task_id, owner, vendor, model, vendor_session_id, state, role, created_by, created_at, forked_from, forked_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, sess.Id, nullable(sess.TaskId), sess.Owner, string(sess.Vendor), sess.Model, sess.VendorSessionId, string(sess.State), string(sess.Role), sess.CreatedBy, formatTime(sess.CreatedAt), forkedFrom, forkedAt)
|
||||
INSERT INTO agent_sessions (id, task_id, owner, vendor, model, vendor_session_id, state, role, member, created_by, created_at, forked_from, forked_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, sess.Id, nullable(sess.TaskId), sess.Owner, string(sess.Vendor), sess.Model, sess.VendorSessionId, string(sess.State), string(sess.Role), sess.Member, sess.CreatedBy, formatTime(sess.CreatedAt), forkedFrom, forkedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1836,7 +1850,7 @@ const taskStateExpr = `CASE
|
||||
END
|
||||
FROM agent_sessions s
|
||||
WHERE s.task_id = tasks.id AND s.role != 'reviewer' AND s.state != 'completed'
|
||||
ORDER BY s.created_at DESC LIMIT 1
|
||||
ORDER BY s.state = 'running' DESC, s.subagents > 0 DESC, s.created_at DESC LIMIT 1
|
||||
), 'no_agent')
|
||||
END`
|
||||
|
||||
@@ -1895,7 +1909,7 @@ const projectColumns = `id, name, repo_path, default_branch, remote, created_by,
|
||||
|
||||
const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at, workflow_name, workflow_run, workflow_stage, parent_task_id, parent_merge_by, merge_after_review_by != '', state, merging_by != '' OR finalizing != '', discard_state != '' AND status = 'active', (SELECT coalesce(max(seq), 0) FROM events WHERE events.task_id = tasks.id AND kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1)`
|
||||
|
||||
const sessionColumns = `id, coalesce(task_id, ''), owner, vendor, model, vendor_session_id, state, role, created_by, created_at, forked_from, forked_at, subagents`
|
||||
const sessionColumns = `id, coalesce(task_id, ''), owner, vendor, model, vendor_session_id, state, role, member, created_by, created_at, forked_from, forked_at, subagents`
|
||||
|
||||
type scanner interface {
|
||||
Scan(dest ...any) error
|
||||
@@ -1985,7 +1999,7 @@ func scanSession(row scanner) (api.AgentSession, error) {
|
||||
createdAt := ""
|
||||
forkedFrom := ""
|
||||
forkedAt := ""
|
||||
err := row.Scan(&sess.Id, &sess.TaskId, &sess.Owner, &vendor, &sess.Model, &sess.VendorSessionId, &state, &role, &sess.CreatedBy, &createdAt, &forkedFrom, &forkedAt, &sess.Subagents)
|
||||
err := row.Scan(&sess.Id, &sess.TaskId, &sess.Owner, &vendor, &sess.Model, &sess.VendorSessionId, &state, &role, &sess.Member, &sess.CreatedBy, &createdAt, &forkedFrom, &forkedAt, &sess.Subagents)
|
||||
if err != nil {
|
||||
return sess, err
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TaskInput struct {
|
||||
ID int64
|
||||
TaskID string
|
||||
Member string
|
||||
Actor string
|
||||
Text string
|
||||
Images []string
|
||||
@@ -26,8 +28,8 @@ func (s *Store) AddTaskInput(ctx context.Context, in TaskInput, projectID, sessi
|
||||
payload["images"] = in.Images
|
||||
}
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
res, err := tx.ExecContext(ctx, `INSERT INTO task_inputs (task_id, actor, text, images, created_at) VALUES (?, ?, ?, ?, ?)`,
|
||||
in.TaskID, in.Actor, in.Text, string(images), now())
|
||||
res, err := tx.ExecContext(ctx, `INSERT INTO task_inputs (task_id, member, actor, text, images, created_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
in.TaskID, in.Member, in.Actor, in.Text, string(images), now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -39,25 +41,28 @@ func (s *Store) AddTaskInput(ctx context.Context, in TaskInput, projectID, sessi
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) WithdrawTaskInput(ctx context.Context, taskID string, id int64, projectID, sessionID, actor string) error {
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
res, err := tx.ExecContext(ctx, `DELETE FROM task_inputs WHERE id = ? AND task_id = ?`, id, taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
func (s *Store) WithdrawTaskInput(ctx context.Context, taskID string, id int64, projectID, actor string) (string, error) {
|
||||
sessionID := ""
|
||||
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
member := ""
|
||||
err := tx.QueryRowContext(ctx, `DELETE FROM task_inputs WHERE id = ? AND task_id = ? RETURNING member`, id, taskID).Scan(&member)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tx.QueryRowContext(ctx, `SELECT id FROM agent_sessions WHERE task_id = ? AND member = ? AND role = 'implementer' ORDER BY created_at DESC LIMIT 1`, taskID, member).Scan(&sessionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return emit(sessionEvent("user.unqueued", projectID, taskID, "", sessionID, actor, map[string]any{"id": id}))
|
||||
})
|
||||
return sessionID, err
|
||||
}
|
||||
|
||||
func (s *Store) TaskInputs(ctx context.Context, taskID string) ([]TaskInput, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT id, task_id, actor, text, images, created_at FROM task_inputs WHERE task_id = ? ORDER BY id`, taskID)
|
||||
func (s *Store) TaskInputs(ctx context.Context, taskID, member string) ([]TaskInput, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT id, task_id, member, actor, text, images, created_at FROM task_inputs WHERE task_id = ? AND member = ? ORDER BY id`, taskID, member)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -68,7 +73,7 @@ func (s *Store) TaskInputs(ctx context.Context, taskID string) ([]TaskInput, err
|
||||
in := TaskInput{}
|
||||
images := ""
|
||||
createdAt := ""
|
||||
err = rows.Scan(&in.ID, &in.TaskID, &in.Actor, &in.Text, &images, &createdAt)
|
||||
err = rows.Scan(&in.ID, &in.TaskID, &in.Member, &in.Actor, &in.Text, &images, &createdAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -92,8 +97,8 @@ func (s *Store) RestoreTaskInputs(ctx context.Context, inputs []TaskInput) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `INSERT INTO task_inputs (id, task_id, actor, text, images, created_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
in.ID, in.TaskID, in.Actor, in.Text, string(images), formatTime(in.CreatedAt))
|
||||
_, err = tx.ExecContext(ctx, `INSERT INTO task_inputs (id, task_id, member, actor, text, images, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
in.ID, in.TaskID, in.Member, in.Actor, in.Text, string(images), formatTime(in.CreatedAt))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user