package store import ( "context" "crypto/rand" "database/sql" "embed" "encoding/hex" "encoding/json" "errors" "fmt" "strconv" "strings" "sync" "time" _ "modernc.org/sqlite" "g.fc.run/ai/byre/internal/api" ) var ErrNotFound = errors.New("not found") //go:embed migrations/*.sql var migrationFiles embed.FS type migration struct { name string sql string } func loadMigrations() ([]migration, error) { entries, err := migrationFiles.ReadDir("migrations") if err != nil { return nil, err } migrations := []migration{} for _, e := range entries { num, name, ok := strings.Cut(strings.TrimSuffix(e.Name(), ".sql"), "-") _, err = strconv.Atoi(num) if !ok || len(num) != 3 || err != nil { return nil, fmt.Errorf("migration file %s: want NNN-name.sql", e.Name()) } sql, err := migrationFiles.ReadFile("migrations/" + e.Name()) if err != nil { return nil, err } migrations = append(migrations, migration{name: name, sql: string(sql)}) } return migrations, nil } const ( DefaultPortFirst = 41000 PortBlock = 10 TaskColors = 11 ) type Store struct { db *sql.DB path string portFirst int writeMu sync.Mutex onEvent func([]api.Event) } func Open(path string, portFirst int, onEvent func([]api.Event)) (*Store, error) { dsn := "file:" + path + "?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)&_txlock=immediate" db, err := sql.Open("sqlite", dsn) if err != nil { return nil, err } err = migrate(db) if err != nil { db.Close() return nil, err } return &Store{db: db, path: path, portFirst: portFirst, onEvent: onEvent}, nil } func migrate(db *sql.DB) error { migrations, err := loadMigrations() if err != nil { return err } _, err = db.Exec(`CREATE TABLE IF NOT EXISTS migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)`) if err != nil { return err } applied, err := appliedMigrations(db) if err != nil { return err } for _, m := range migrations { if applied[m.name] { continue } err = applyMigration(db, m) if err != nil { return fmt.Errorf("migration %s: %w", m.name, err) } } return nil } func appliedMigrations(db *sql.DB) (map[string]bool, error) { rows, err := db.Query(`SELECT name FROM migrations`) if err != nil { return nil, err } defer rows.Close() applied := map[string]bool{} for rows.Next() { name := "" err = rows.Scan(&name) if err != nil { return nil, err } applied[name] = true } return applied, rows.Err() } func applyMigration(db *sql.DB, m migration) error { ctx := context.Background() conn, err := db.Conn(ctx) if err != nil { return err } defer conn.Close() _, err = conn.ExecContext(ctx, `PRAGMA foreign_keys = OFF`) if err != nil { return err } defer conn.ExecContext(ctx, `PRAGMA foreign_keys = ON`) tx, err := conn.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() _, err = tx.Exec(m.sql) if err != nil { return err } err = checkForeignKeys(tx) if err != nil { return err } _, err = tx.Exec(`INSERT INTO migrations (name, applied_at) VALUES (?, ?)`, m.name, now()) if err != nil { return err } return tx.Commit() } func checkForeignKeys(tx *sql.Tx) error { rows, err := tx.Query(`PRAGMA foreign_key_check`) if err != nil { return err } defer rows.Close() if rows.Next() { table := "" rowid := sql.NullInt64{} parent := "" fkid := 0 err = rows.Scan(&table, &rowid, &parent, &fkid) if err != nil { return err } return fmt.Errorf("foreign key violation: %s row %d references a missing %s", table, rowid.Int64, parent) } return rows.Err() } func (s *Store) Close() error { return s.db.Close() } func (s *Store) Path() string { return s.path } func NewID() string { b := make([]byte, 6) rand.Read(b) return hex.EncodeToString(b) } func (s *Store) EnsureUser(ctx context.Context, login, displayName string) (api.User, error) { _, err := s.db.ExecContext(ctx, ` INSERT INTO users (login, display_name, created_at) VALUES (?, ?, ?) ON CONFLICT (login) DO UPDATE SET display_name = excluded.display_name `, login, displayName, now()) if err != nil { return api.User{}, err } return s.GetUser(ctx, login) } func (s *Store) GetUser(ctx context.Context, login string) (api.User, error) { u := api.User{} prefs := "" err := s.db.QueryRowContext(ctx, `SELECT login, display_name, git_name, git_email, notification_prefs, default_model, review_model, second_review_model FROM users WHERE login = ?`, login). Scan(&u.Login, &u.DisplayName, &u.GitName, &u.GitEmail, &prefs, &u.DefaultModel, &u.ReviewModel, &u.SecondReviewModel) if errors.Is(err, sql.ErrNoRows) { return u, ErrNotFound } if err != nil { return u, err } err = json.Unmarshal([]byte(prefs), &u.NotificationPrefs) return u, err } func (s *Store) UpdateUserGit(ctx context.Context, login, gitName, gitEmail string) (api.User, error) { err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { _, err := tx.ExecContext(ctx, `UPDATE users SET git_name = ?, git_email = ? WHERE login = ?`, gitName, gitEmail, login) if err != nil { return err } return emit(api.Event{Kind: "user.updated", Actor: login, Payload: map[string]any{"login": login}}) }) if err != nil { return api.User{}, err } return s.GetUser(ctx, login) } func (s *Store) CreateProject(ctx context.Context, p api.Project) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { _, err := tx.ExecContext(ctx, ` INSERT INTO projects (id, name, repo_path, default_branch, remote, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, ?) `, p.Id, p.Name, p.RepoPath, p.DefaultBranch, p.Remote, p.CreatedBy, formatTime(p.CreatedAt)) if err != nil { return err } return emit(api.Event{Kind: "project.created", ProjectId: &p.Id, Actor: p.CreatedBy, Payload: ToPayload(p)}) }) } func (s *Store) ListProjects(ctx context.Context) ([]api.Project, error) { rows, err := s.db.QueryContext(ctx, `SELECT `+projectColumns+` FROM projects ORDER BY name`) if err != nil { return nil, err } defer rows.Close() projects := []api.Project{} for rows.Next() { p, err := scanProject(rows) if err != nil { return nil, err } projects = append(projects, p) } return projects, rows.Err() } func (s *Store) GetProject(ctx context.Context, id string) (api.Project, error) { p, err := scanProject(s.db.QueryRowContext(ctx, `SELECT `+projectColumns+` FROM projects WHERE id = ?`, id)) if errors.Is(err, sql.ErrNoRows) { return p, ErrNotFound } return p, err } func (s *Store) CreateTask(ctx context.Context, t api.Task, frozen *FrozenWorkflow, actor string) error { workflowName := "" workflowDefinition := "" workflowBrief := "" workflowTitle := "" workflowRun := "" workflowStage := "" if frozen != nil { b, err := json.Marshal(frozen.Definition) if err != nil { return err } workflowName = frozen.Name workflowDefinition = string(b) workflowBrief = frozen.Brief workflowTitle = frozen.Title workflowRun = *t.WorkflowRun workflowStage = *t.WorkflowStage t.Workflow = &workflowName } todoItem := "" if t.TodoItem != nil { todoItem = *t.TodoItem } mergeTitle := "" if t.MergeTitle != t.Title { mergeTitle = t.MergeTitle } forkedFrom := "" if t.ForkedFrom != nil { forkedFrom = *t.ForkedFrom } forkedAt := 0 if t.ForkedAt != nil { forkedAt = *t.ForkedAt } parentTaskID := "" if t.ParentTaskId != nil { parentTaskID = *t.ParentTaskId } return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { if parentTaskID != "" { open := false err := tx.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM tasks WHERE id = ? AND status = ? AND discard_state = '' AND finalizing = '')`, parentTaskID, string(api.TaskStatusActive)).Scan(&open) if err != nil { return err } if !open { return ErrParentNotOpen } } err := tx.QueryRowContext(ctx, ` SELECT min(candidate) FROM ( SELECT ? AS candidate UNION ALL SELECT port_base + ? FROM tasks WHERE status = ? ) WHERE candidate NOT IN (SELECT port_base FROM tasks WHERE status = ?) `, s.portFirst, PortBlock, string(api.TaskStatusActive), string(api.TaskStatusActive)).Scan(&t.PortBase) if err != nil { return err } err = tx.QueryRowContext(ctx, ` WITH RECURSIVE palette(c) AS (SELECT 0 UNION ALL SELECT c + 1 FROM palette WHERE c + 1 < ?) SELECT c FROM palette ORDER BY c IN (SELECT color FROM tasks WHERE status = ?), coalesce((SELECT max(rowid) FROM tasks WHERE color = c), -1), c LIMIT 1 `, TaskColors, string(api.TaskStatusActive)).Scan(&t.Color) if err != nil { return err } _, err = tx.ExecContext(ctx, ` INSERT INTO tasks (id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, todo_item, forked_from, forked_at, workflow_name, workflow_definition, workflow_brief, workflow_title, workflow_run, workflow_stage, parent_task_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, t.Id, t.ProjectId, t.Title, mergeTitle, t.Branch, t.WorktreePath, t.BaseRef, t.PortBase, t.Color, string(t.Status), t.CreatedBy, formatTime(t.CreatedAt), todoItem, forkedFrom, forkedAt, workflowName, workflowDefinition, workflowBrief, workflowTitle, workflowRun, workflowStage, parentTaskID) if err != nil { return err } return emit(api.Event{Kind: "task.created", ProjectId: &t.ProjectId, TaskId: &t.Id, Actor: actor, Payload: ToPayload(t)}) }) } func (s *Store) ListTasks(ctx context.Context, projectID string) ([]api.Task, error) { return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE project_id = ? ORDER BY created_at DESC`, projectID) } func (s *Store) GetTask(ctx context.Context, id string) (api.Task, error) { t, err := scanTask(s.db.QueryRowContext(ctx, `SELECT `+taskColumns+` FROM tasks WHERE id = ?`, id)) if errors.Is(err, sql.ErrNoRows) { return t, ErrNotFound } return t, err } func (s *Store) SetMergeTitle(ctx context.Context, id, title, actor string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { projectID := "" err := tx.QueryRowContext(ctx, `UPDATE tasks SET merge_title = ? WHERE id = ? AND status = ? AND merging_by = '' AND finalizing = '' AND merge_queued_by = '' RETURNING project_id`, title, id, string(api.TaskStatusActive)).Scan(&projectID) if errors.Is(err, sql.ErrNoRows) { return ErrNotFound } if err != nil { return err } return emit(api.Event{Kind: "task.merge_title", ProjectId: &projectID, TaskId: &id, Actor: actor, Payload: map[string]any{"id": id, "merge_title": title}}) }) } func (s *Store) SetMergingBy(ctx context.Context, id, login string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { projectID := "" err := tx.QueryRowContext(ctx, `UPDATE tasks SET merging_by = ? WHERE id = ? AND discard_state = '' AND finalizing = '' RETURNING project_id`, login, id).Scan(&projectID) if errors.Is(err, sql.ErrNoRows) { return ErrNotFound } if err != nil { return err } return emit(api.Event{Kind: "task.merge_started", ProjectId: &projectID, TaskId: &id, Actor: login, Payload: map[string]any{"id": id}}) }) } func (s *Store) MergingBy(ctx context.Context, id string) (string, error) { login := "" err := s.db.QueryRowContext(ctx, `SELECT merging_by FROM tasks WHERE id = ?`, id).Scan(&login) if errors.Is(err, sql.ErrNoRows) { return "", ErrNotFound } return login, err } func (s *Store) TakeMergingBy(ctx context.Context, id string) (string, error) { login := "" err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { projectID := "" err := tx.QueryRowContext(ctx, `SELECT project_id, merging_by FROM tasks WHERE id = ?`, id).Scan(&projectID, &login) if errors.Is(err, sql.ErrNoRows) { return ErrNotFound } if err != nil { return err } if login == "" { return nil } _, err = tx.ExecContext(ctx, `UPDATE tasks SET merging_by = '' WHERE id = ?`, id) if err != nil { return err } return emit(api.Event{Kind: "task.merge_ended", ProjectId: &projectID, TaskId: &id, Actor: "byre", Payload: map[string]any{"id": id}}) }) return login, err } func (s *Store) ClearFinalizing(ctx context.Context, id string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { projectID := "" err := tx.QueryRowContext(ctx, `UPDATE tasks SET finalizing = '', finalizing_commit = '', finalizing_notes = '', finalizing_unchanged = 0 WHERE id = ? AND finalizing != '' RETURNING project_id`, id).Scan(&projectID) if errors.Is(err, sql.ErrNoRows) { return nil } if err != nil { return err } return emit(api.Event{Kind: "task.merge_ended", ProjectId: &projectID, TaskId: &id, Actor: "byre", Payload: map[string]any{"id": id}}) }) } func (s *Store) SetFinalizingCommit(ctx context.Context, id, commit, notes string) error { return s.setFinalizing(ctx, id, commit, notes, false) } func (s *Store) SetFinalizingUnchanged(ctx context.Context, id string) error { return s.setFinalizing(ctx, id, "", "", true) } func (s *Store) setFinalizing(ctx context.Context, id, commit, notes string, unchanged bool) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { res, err := tx.ExecContext(ctx, `UPDATE tasks SET finalizing_commit = ?, finalizing_notes = ?, finalizing_unchanged = ? WHERE id = ? AND status = ? AND finalizing != ''`, commit, notes, unchanged, id, string(api.TaskStatusActive)) if err != nil { return err } n, err := res.RowsAffected() if err != nil { return err } if n == 0 { return ErrNotFound } return nil }) } type FinalizingMerge struct { Task api.Task By string Commit string Notes string Unchanged bool } func (s *Store) FinalizingMerges(ctx context.Context) ([]FinalizingMerge, error) { rows, err := s.db.QueryContext(ctx, `SELECT `+taskColumns+`, finalizing, finalizing_commit, finalizing_notes, finalizing_unchanged FROM tasks WHERE status = ? AND finalizing != ''`, string(api.TaskStatusActive)) if err != nil { return nil, err } defer rows.Close() merges := []FinalizingMerge{} for rows.Next() { f := FinalizingMerge{} f.Task, err = scanTask(trailingScanner{rows, []any{&f.By, &f.Commit, &f.Notes, &f.Unchanged}}) if err != nil { return nil, err } merges = append(merges, f) } return merges, rows.Err() } func (s *Store) MergingTasks(ctx context.Context) ([]api.Task, error) { return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE status = ? AND merging_by != ''`, string(api.TaskStatusActive)) } func (s *Store) QueuedMerges(ctx context.Context, projectID string) ([]api.Task, error) { return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE project_id = ? AND status = ? AND merge_queued_by != '' ORDER BY merge_queued_at`, projectID, string(api.TaskStatusActive)) } func (s *Store) ProjectsWithQueuedMerges(ctx context.Context) ([]string, error) { rows, err := s.db.QueryContext(ctx, `SELECT DISTINCT project_id FROM tasks WHERE status = ? AND merge_queued_by != ''`, string(api.TaskStatusActive)) if err != nil { return nil, err } defer rows.Close() ids := []string{} for rows.Next() { id := "" err = rows.Scan(&id) if err != nil { return nil, err } ids = append(ids, id) } return ids, rows.Err() } func (s *Store) queryTasks(ctx context.Context, query string, args ...any) ([]api.Task, error) { rows, err := s.db.QueryContext(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() tasks := []api.Task{} for rows.Next() { t, err := scanTask(rows) if err != nil { return nil, err } tasks = append(tasks, t) } return tasks, rows.Err() } func (s *Store) ActiveSubtasks(ctx context.Context, parentID string) ([]api.Task, error) { return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE parent_task_id = ? AND status = ? ORDER BY created_at`, parentID, string(api.TaskStatusActive)) } func (s *Store) SetParentMerge(ctx context.Context, id, login string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { projectID, parentID, status, parentMergeBy, discardState, finalizing := "", "", "", "", "", "" err := tx.QueryRowContext(ctx, `SELECT project_id, parent_task_id, status, parent_merge_by, discard_state, finalizing FROM tasks WHERE id = ?`, id). Scan(&projectID, &parentID, &status, &parentMergeBy, &discardState, &finalizing) if errors.Is(err, sql.ErrNoRows) { return ErrNotFound } if err != nil { return err } switch { case status != string(api.TaskStatusActive) || parentID == "": return ErrNotFound case discardState != "": return ErrDiscardPending case finalizing != "": return ErrMerging case parentMergeBy != "": return ErrParentMergeRequested } _, err = tx.ExecContext(ctx, `UPDATE tasks SET parent_merge_by = ? WHERE id = ?`, login, id) if err != nil { return err } return emit(api.Event{Kind: "task.parent_merge_started", ProjectId: &projectID, TaskId: &id, Actor: login, Payload: map[string]any{"id": id, "parent_task_id": parentID}}) }) } func (s *Store) ClearParentMerge(ctx context.Context, id string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { projectID := "" err := tx.QueryRowContext(ctx, `UPDATE tasks SET parent_merge_by = '' WHERE id = ? AND parent_merge_by != '' RETURNING project_id`, id).Scan(&projectID) if errors.Is(err, sql.ErrNoRows) { return nil } if err != nil { return err } return emit(api.Event{Kind: "task.parent_merge_stopped", ProjectId: &projectID, TaskId: &id, Actor: "byre", Payload: map[string]any{"id": id}}) }) } func (s *Store) ParentMergeBy(ctx context.Context, id string) (string, error) { login := "" err := s.db.QueryRowContext(ctx, `SELECT parent_merge_by FROM tasks WHERE id = ?`, id).Scan(&login) if errors.Is(err, sql.ErrNoRows) { return "", ErrNotFound } return login, err } func (s *Store) QueueMerge(ctx context.Context, id, login string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { projectID := "" err := tx.QueryRowContext(ctx, `UPDATE tasks SET merge_queued_by = ?, merge_queued_at = ? WHERE id = ? AND status = ? AND merge_queued_by = '' AND discard_state = '' RETURNING project_id`, login, now(), id, string(api.TaskStatusActive)).Scan(&projectID) if errors.Is(err, sql.ErrNoRows) { return ErrNotFound } if err != nil { return err } return emit(api.Event{Kind: "task.merge_queued", ProjectId: &projectID, TaskId: &id, Actor: login, Payload: map[string]any{"id": id}}) }) } func (s *Store) TakeQueuedMerge(ctx context.Context, id string) (string, error) { login := "" err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { projectID := "" err := tx.QueryRowContext(ctx, `SELECT project_id, merge_queued_by FROM tasks WHERE id = ?`, id).Scan(&projectID, &login) if errors.Is(err, sql.ErrNoRows) { return ErrNotFound } if err != nil { return err } if login == "" { return nil } _, err = tx.ExecContext(ctx, `UPDATE tasks SET merge_queued_by = '', merge_queued_at = '' WHERE id = ?`, id) if err != nil { return err } return emit(api.Event{Kind: "task.merge_unqueued", ProjectId: &projectID, TaskId: &id, Actor: "byre", Payload: map[string]any{"id": id}}) }) return login, err } type MergeAfterReview struct { By string ReviewID string Target api.MergeTaskJSONBodyTarget } func (s *Store) SetMergeAfterReview(ctx context.Context, id string, mar MergeAfterReview) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { projectID := "" snapshotN := 0 err := tx.QueryRowContext(ctx, ` UPDATE tasks SET merge_after_review_by = ?, merge_after_review_id = ?, merge_after_review_target = ? WHERE id = ? AND status = ? AND discard_state = '' AND merging_by = '' AND finalizing = '' AND merge_queued_by = '' AND parent_merge_by = '' AND EXISTS (SELECT 1 FROM reviews WHERE reviews.id = ? AND reviews.task_id = tasks.id AND reviews.state = ?) RETURNING project_id, (SELECT snapshot_n FROM reviews WHERE reviews.id = ?)`, mar.By, mar.ReviewID, string(mar.Target), id, string(api.TaskStatusActive), mar.ReviewID, string(api.ReviewStateRunning), mar.ReviewID).Scan(&projectID, &snapshotN) if errors.Is(err, sql.ErrNoRows) { return ErrNotFound } if err != nil { return err } return emit(api.Event{Kind: "task.merge_after_review", ProjectId: &projectID, TaskId: &id, Actor: mar.By, Payload: map[string]any{"id": id, "review_id": mar.ReviewID, "snapshot_n": snapshotN, "target": string(mar.Target)}}) }) } func (s *Store) CancelMergeAfterReview(ctx context.Context, id, actor string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { projectID := "" err := tx.QueryRowContext(ctx, `UPDATE tasks SET merge_after_review_by = '', merge_after_review_id = '', merge_after_review_target = '' WHERE id = ? AND merge_after_review_by != '' RETURNING project_id`, id).Scan(&projectID) if errors.Is(err, sql.ErrNoRows) { return ErrNotFound } if err != nil { return err } return emit(api.Event{Kind: "task.merge_after_review_cancelled", ProjectId: &projectID, TaskId: &id, Actor: actor, Payload: map[string]any{"id": id}}) }) } func (s *Store) FinishedMergeAfterReviews(ctx context.Context) ([]string, error) { rows, err := s.db.QueryContext(ctx, ` SELECT tasks.merge_after_review_id FROM tasks JOIN reviews ON reviews.id = tasks.merge_after_review_id WHERE tasks.merge_after_review_by != '' AND reviews.state != ?`, string(api.ReviewStateRunning)) if err != nil { return nil, err } defer rows.Close() ids := []string{} for rows.Next() { id := "" err = rows.Scan(&id) if err != nil { return nil, err } ids = append(ids, id) } return ids, rows.Err() } func (s *Store) TakeMergeAfterReview(ctx context.Context, id, reviewID string) (MergeAfterReview, error) { mar := MergeAfterReview{} err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { projectID, target := "", "" err := tx.QueryRowContext(ctx, `SELECT project_id, merge_after_review_by, merge_after_review_id, merge_after_review_target FROM tasks WHERE id = ?`, id). Scan(&projectID, &mar.By, &mar.ReviewID, &target) if errors.Is(err, sql.ErrNoRows) { return ErrNotFound } if err != nil { return err } mar.Target = api.MergeTaskJSONBodyTarget(target) if mar.By == "" || (reviewID != "" && mar.ReviewID != reviewID) { mar = MergeAfterReview{} return nil } _, err = tx.ExecContext(ctx, `UPDATE tasks SET merge_after_review_by = '', merge_after_review_id = '', merge_after_review_target = '' WHERE id = ?`, id) if err != nil { return err } return emit(api.Event{Kind: "task.merge_after_review_ended", ProjectId: &projectID, TaskId: &id, Actor: "byre", Payload: map[string]any{"id": id}}) }) return mar, err } func (s *Store) SetTaskSummary(ctx context.Context, id, summary, actor string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { projectID := "" err := tx.QueryRowContext(ctx, `UPDATE tasks SET summary = ? WHERE id = ? AND status = ? RETURNING project_id`, summary, id, string(api.TaskStatusActive)).Scan(&projectID) if errors.Is(err, sql.ErrNoRows) { return ErrNotFound } if err != nil { return err } return emit(api.Event{Kind: "task.summary", ProjectId: &projectID, TaskId: &id, Actor: actor, Payload: map[string]any{"id": id, "summary": summary}}) }) } func (s *Store) ReadyPromptN(ctx context.Context, id string) (int, error) { n := 0 err := s.db.QueryRowContext(ctx, `SELECT ready_prompt_n FROM tasks WHERE id = ?`, id).Scan(&n) if errors.Is(err, sql.ErrNoRows) { return 0, ErrNotFound } return n, err } func (s *Store) SetReadyPromptN(ctx context.Context, id string, snapshotN int) error { _, err := s.db.ExecContext(ctx, `UPDATE tasks SET ready_prompt_n = ? WHERE id = ?`, snapshotN, id) return err } func (s *Store) MarkReady(ctx context.Context, id, overview, actor string) error { return s.emitActive(ctx, id, actor, "task.ready", map[string]any{"id": id, "overview": overview}) } func (s *Store) SuggestDiscard(ctx context.Context, id, reason, actor string) error { return s.emitActive(ctx, id, actor, "task.discard_suggested", map[string]any{"id": id, "reason": reason}) } func (s *Store) emitActive(ctx context.Context, id, actor, kind string, payload map[string]any) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { projectID := "" err := tx.QueryRowContext(ctx, `SELECT project_id FROM tasks WHERE id = ? AND status = ?`, id, string(api.TaskStatusActive)).Scan(&projectID) if errors.Is(err, sql.ErrNoRows) { return ErrNotFound } if err != nil { return err } return emit(api.Event{Kind: kind, ProjectId: &projectID, TaskId: &id, Actor: actor, Payload: payload}) }) } func (s *Store) TaskSummary(ctx context.Context, id string) (string, error) { summary := "" err := s.db.QueryRowContext(ctx, `SELECT summary FROM tasks WHERE id = ?`, id).Scan(&summary) if errors.Is(err, sql.ErrNoRows) { return "", ErrNotFound } return summary, err } type ClosedTask struct { Task api.Task Project string Summary string } func (s *Store) SearchClosedTasks(ctx context.Context, projectID, query string, limit int) ([]ClosedTask, error) { pattern := "%" + strings.NewReplacer(`\`, `\\`, "%", `\%`, "_", `\_`).Replace(query) + "%" rows, err := s.db.QueryContext(ctx, ` SELECT `+taskColumns+`, (SELECT name FROM projects WHERE projects.id = tasks.project_id), summary FROM tasks WHERE status != ? AND (? = '' OR project_id = ?) AND (title LIKE ? ESCAPE '\' OR merge_title LIKE ? ESCAPE '\' OR todo_item LIKE ? ESCAPE '\' OR summary LIKE ? ESCAPE '\' OR merge_commit LIKE ? ESCAPE '\') ORDER BY closed_at DESC LIMIT ?`, string(api.TaskStatusActive), projectID, projectID, pattern, pattern, pattern, pattern, pattern, limit) if err != nil { return nil, err } defer rows.Close() found := []ClosedTask{} for rows.Next() { c := ClosedTask{} c.Task, err = scanTask(trailingScanner{rows, []any{&c.Project, &c.Summary}}) if err != nil { return nil, err } found = append(found, c) } return found, rows.Err() } type trailingScanner struct { row scanner extra []any } func (t trailingScanner) Scan(dest ...any) error { return t.row.Scan(append(dest, t.extra...)...) } func (s *Store) MergeTask(ctx context.Context, id, commit, pushed, actor string) (api.Task, error) { return s.closeTask(ctx, id, api.TaskStatusMerged, commit, actor, "task.merged", map[string]any{"id": id, "commit": commit, "pushed": pushed}) } func (s *Store) MergeTaskIntoParent(ctx context.Context, id, parentID, commit, actor string) (api.Task, error) { return s.closeTask(ctx, id, api.TaskStatusMerged, commit, actor, "task.merged", map[string]any{"id": id, "commit": commit, "parent_task_id": parentID}) } func (s *Store) DiscardTask(ctx context.Context, id, actor string) (api.Task, error) { return s.closeTask(ctx, id, api.TaskStatusDiscarded, "", actor, "task.discarded", map[string]any{"id": id}) } func (s *Store) closeTask(ctx context.Context, id string, status api.TaskStatus, commit, actor, kind string, payload map[string]any) (api.Task, error) { err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { res, err := tx.ExecContext(ctx, `UPDATE tasks SET status = ?, closed_at = ?, merge_commit = ?, finalizing = '', finalizing_commit = '', finalizing_notes = '', finalizing_unchanged = 0, merge_after_review_by = '', merge_after_review_id = '', merge_after_review_target = '' WHERE id = ? AND status = ?`, string(status), now(), commit, id, string(api.TaskStatusActive)) if err != nil { return err } n, err := res.RowsAffected() if err != nil { return err } if n == 0 { return ErrNotFound } projectID := "" err = tx.QueryRowContext(ctx, `SELECT project_id FROM tasks WHERE id = ?`, id).Scan(&projectID) if err != nil { return err } err = emit(api.Event{Kind: kind, ProjectId: &projectID, TaskId: &id, Actor: actor, Payload: payload}) if err != nil { return err } _, err = tx.ExecContext(ctx, `DELETE FROM task_inputs WHERE task_id = ?`, id) if err != nil { return err } return dismissTaskInbox(ctx, tx, emit, id, actor) }) if err != nil { return api.Task{}, err } return s.GetTask(ctx, id) } func (s *Store) Participants(ctx context.Context, taskID string) ([]api.Participant, error) { 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 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 { return nil, err } defer rows.Close() participants := []api.Participant{} for rows.Next() { p := api.Participant{} err = rows.Scan(&p.Login, &p.DisplayName, &p.GitName, &p.GitEmail) if err != nil { return nil, err } participants = append(participants, p) } return participants, rows.Err() } const eventColumns = `seq, kind, project_id, task_id, agent_session_id, owner, actor, payload, created_at` func (s *Store) EventsAfter(ctx context.Context, viewer string, after int64, limit int) ([]api.Event, error) { rows, err := s.db.QueryContext(ctx, ` SELECT `+eventColumns+` FROM events WHERE seq > ? AND (owner IS NULL OR owner = ?) ORDER BY seq LIMIT ? `, after, viewer, limit) if err != nil { return nil, err } return scanEvents(rows) } func (s *Store) TaskEvents(ctx context.Context, taskID string, after int64, limit int) ([]api.Event, error) { rows, err := s.db.QueryContext(ctx, ` SELECT `+eventColumns+` FROM events WHERE task_id = ? AND seq > ? ORDER BY seq LIMIT ? `, taskID, after, limit) if err != nil { return nil, err } return scanEvents(rows) } func (s *Store) FirstPrompt(ctx context.Context, taskID string) (string, error) { text := "" err := s.db.QueryRowContext(ctx, ` SELECT coalesce(json_extract(payload, '$.text'), '') FROM events WHERE task_id = ? AND kind = 'user.message' ORDER BY seq LIMIT 1 `, taskID).Scan(&text) return text, err } 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 `, taskID, limit) if err != nil { return nil, err } return scanEvents(rows) } type Handoff struct { SessionID string At time.Time Text string } func (s *Store) TaskHandoffs(ctx context.Context, taskID string) ([]Handoff, error) { rows, err := s.db.QueryContext(ctx, ` SELECT agent_session_id, created_at, coalesce(json_extract(payload, '$.result'), '') FROM events WHERE task_id = ? AND kind = 'turn.ended' AND json_extract(payload, '$.handoff') = 1 ORDER BY seq `, taskID) if err != nil { return nil, err } defer rows.Close() handoffs := []Handoff{} for rows.Next() { h := Handoff{} at := "" err = rows.Scan(&h.SessionID, &at, &h.Text) if err != nil { return nil, err } h.At, err = parseTime(at) if err != nil { return nil, err } handoffs = append(handoffs, h) } return handoffs, rows.Err() } func (s *Store) LeadEvents(ctx context.Context, owner string, after int64, limit int) ([]api.Event, error) { rows, err := s.db.QueryContext(ctx, ` SELECT `+eventColumns+` FROM events WHERE owner = ? AND agent_session_id IS NOT NULL AND seq > ? ORDER BY seq LIMIT ? `, owner, after, limit) if err != nil { return nil, err } return scanEvents(rows) } func (s *Store) LeadEventsPage(ctx context.Context, owner string, before int64, turns int) (api.EventPage, error) { from := int64(0) more := true err := s.db.QueryRowContext(ctx, ` SELECT seq FROM events WHERE owner = ? AND agent_session_id IS NOT NULL AND kind = 'turn.ended' AND seq < ? ORDER BY seq DESC LIMIT 1 OFFSET ? `, owner, before, turns).Scan(&from) if errors.Is(err, sql.ErrNoRows) { more = false } else if err != nil { return api.EventPage{}, err } rows, err := s.db.QueryContext(ctx, ` SELECT `+eventColumns+` FROM events WHERE owner = ? AND agent_session_id IS NOT NULL AND seq > ? AND seq < ? ORDER BY seq `, owner, from, before) if err != nil { return api.EventPage{}, err } events, err := scanEvents(rows) if err != nil { return api.EventPage{}, err } return api.EventPage{Events: events, More: more}, nil } func scanEvents(rows *sql.Rows) ([]api.Event, error) { defer rows.Close() events := []api.Event{} for rows.Next() { ev := api.Event{} payload := "" createdAt := "" err := rows.Scan(&ev.Seq, &ev.Kind, &ev.ProjectId, &ev.TaskId, &ev.AgentSessionId, &ev.Owner, &ev.Actor, &payload, &createdAt) if err != nil { return nil, err } err = json.Unmarshal([]byte(payload), &ev.Payload) if err != nil { return nil, err } ev.CreatedAt, err = parseTime(createdAt) if err != nil { return nil, err } events = append(events, ev) } return events, rows.Err() } func (s *Store) AppendEvent(ctx context.Context, ev api.Event) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { return emit(ev) }) } func (s *Store) Broadcast(ev api.Event) { s.writeMu.Lock() defer s.writeMu.Unlock() ev.CreatedAt = time.Now().UTC() s.onEvent([]api.Event{ev}) } func (s *Store) CreateSession(ctx context.Context, sess api.AgentSession, projectID string) error { forkedFrom := "" forkedAt := "" if sess.ForkedFrom != nil && sess.ForkedAt != nil { forkedFrom = *sess.ForkedFrom forkedAt = *sess.ForkedAt } 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, 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 } return emit(sessionEvent("session.created", projectID, sess.TaskId, sess.Owner, sess.Id, sess.CreatedBy, ToPayload(sess))) }) } func sessionEvent(kind, projectID, taskID, owner, sessionID, actor string, payload map[string]any) api.Event { return api.Event{ Kind: kind, ProjectId: nullable(projectID), TaskId: nullable(taskID), AgentSessionId: &sessionID, Owner: nullable(owner), Actor: actor, Payload: payload, } } func nullable(s string) *string { if s == "" { return nil } return &s } func (s *Store) LeadSessions(ctx context.Context, owner string) ([]api.AgentSession, error) { return s.querySessions(ctx, `SELECT `+sessionColumns+` FROM agent_sessions WHERE owner = ? ORDER BY created_at`, owner) } func (s *Store) LeadOwners(ctx context.Context) ([]string, error) { rows, err := s.db.QueryContext(ctx, `SELECT DISTINCT owner FROM agent_sessions WHERE owner != '' ORDER BY owner`) if err != nil { return nil, err } defer rows.Close() owners := []string{} for rows.Next() { owner := "" err = rows.Scan(&owner) if err != nil { return nil, err } owners = append(owners, owner) } return owners, rows.Err() } func (s *Store) GetSession(ctx context.Context, id string) (api.AgentSession, error) { sess, err := scanSession(s.db.QueryRowContext(ctx, `SELECT `+sessionColumns+` FROM agent_sessions WHERE id = ?`, id)) if errors.Is(err, sql.ErrNoRows) { return sess, ErrNotFound } return sess, err } func (s *Store) ListSessions(ctx context.Context, taskID string) ([]api.AgentSession, error) { return s.querySessions(ctx, `SELECT `+sessionColumns+` FROM agent_sessions WHERE task_id = ? AND role != ? ORDER BY created_at`, taskID, string(api.Reviewer)) } func (s *Store) SessionsInState(ctx context.Context, state api.AgentSessionState) ([]api.AgentSession, error) { return s.querySessions(ctx, `SELECT `+sessionColumns+` FROM agent_sessions WHERE state = ? ORDER BY created_at`, string(state)) } func (s *Store) querySessions(ctx context.Context, query string, args ...any) ([]api.AgentSession, error) { rows, err := s.db.QueryContext(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() sessions := []api.AgentSession{} for rows.Next() { sess, err := scanSession(rows) if err != nil { return nil, err } sessions = append(sessions, sess) } return sessions, rows.Err() } var ErrStateChanged = errors.New("session state changed") func (s *Store) SetSessionState(ctx context.Context, id string, state api.AgentSessionState, actor string, payload map[string]any) error { return s.transitionSession(ctx, id, nil, state, actor, payload) } func (s *Store) TransitionSession(ctx context.Context, id string, from, to api.AgentSessionState, actor string) error { return s.transitionSession(ctx, id, &from, to, actor, nil) } func (s *Store) transitionSession(ctx context.Context, id string, from *api.AgentSessionState, state api.AgentSessionState, actor string, payload map[string]any) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { taskID := "" projectID := "" owner := "" current := "" role := "" discard := "" finalizing := "" err := tx.QueryRowContext(ctx, ` SELECT coalesce(t.id, ''), coalesce(t.project_id, ''), s.owner, s.state, s.role, coalesce(t.discard_state, ''), coalesce(t.finalizing, '') FROM agent_sessions s LEFT JOIN tasks t ON t.id = s.task_id WHERE s.id = ? `, id).Scan(&taskID, &projectID, &owner, ¤t, &role, &discard, &finalizing) if errors.Is(err, sql.ErrNoRows) { return ErrNotFound } if err != nil { return err } if from != nil && current != string(*from) { return fmt.Errorf("%w: session %s is %s, not %s", ErrStateChanged, id, current, *from) } if state == api.AgentSessionStateRunning && role == string(api.Implementer) && discard != "" { return fmt.Errorf("%w: task %s is being discarded", ErrStateChanged, taskID) } if state == api.AgentSessionStateRunning && role == string(api.Implementer) && finalizing != "" { return fmt.Errorf("%w: task %s is finishing its merge", ErrStateChanged, taskID) } _, err = tx.ExecContext(ctx, `UPDATE agent_sessions SET state = ? WHERE id = ?`, string(state), id) if err != nil { return err } if payload == nil { payload = map[string]any{} } payload["state"] = string(state) return emit(sessionEvent("session.state", projectID, taskID, owner, id, actor, payload)) }) } func (s *Store) SessionsWithSubagents(ctx context.Context) ([]api.AgentSession, error) { return s.querySessions(ctx, `SELECT `+sessionColumns+` FROM agent_sessions WHERE subagents > 0`) } func (s *Store) SetSubagents(ctx context.Context, id string, n int, 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 subagents = ? WHERE id = ?`, n, id) if err != nil { return err } return emit(sessionEvent("session.subagents", projectID, taskID, owner, id, actor, map[string]any{"subagents": n})) }) } func (s *Store) SessionStarted(ctx context.Context, sessionID string) (bool, error) { started := false err := s.db.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM events WHERE agent_session_id = ? AND kind = 'user.message')`, sessionID).Scan(&started) return started, err } type Turn struct { Actor string At time.Time } func (s *Store) RunningTurn(ctx context.Context, taskID string) (Turn, bool, error) { t := Turn{} at := "" err := s.db.QueryRowContext(ctx, ` SELECT e.actor, e.created_at FROM events e JOIN agent_sessions s ON s.id = e.agent_session_id WHERE e.task_id = ? AND e.kind = 'user.message' AND s.role = ? AND s.state = ? ORDER BY e.seq DESC LIMIT 1 `, taskID, string(api.Implementer), string(api.AgentSessionStateRunning)).Scan(&t.Actor, &at) if errors.Is(err, sql.ErrNoRows) { return t, false, nil } if err != nil { return t, false, err } t.At, err = parseTime(at) return t, err == nil, err } func (s *Store) SetVendorSessionID(ctx context.Context, id, vendorSessionID string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { res, err := tx.ExecContext(ctx, `UPDATE agent_sessions SET vendor_session_id = ? WHERE id = ?`, vendorSessionID, id) if err != nil { return err } n, err := res.RowsAffected() if err != nil { return err } if n == 0 { return ErrNotFound } return nil }) } func (s *Store) SetSessionModel(ctx context.Context, id, model, 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 = ? WHERE id = ?`, model, id) if err != nil { return err } return emit(sessionEvent("session.model", projectID, taskID, owner, id, actor, map[string]any{"model": model})) }) } type Counts struct { LastEventSeq int64 Projects int ActiveTasks int } func (s *Store) Counts(ctx context.Context) (Counts, error) { c := Counts{} err := s.db.QueryRowContext(ctx, ` SELECT (SELECT COALESCE(MAX(seq), 0) FROM events), (SELECT COUNT(*) FROM projects), (SELECT COUNT(*) FROM tasks WHERE status = ?) `, string(api.TaskStatusActive)).Scan(&c.LastEventSeq, &c.Projects, &c.ActiveTasks) return c, err } type emitFunc func(api.Event) error func (s *Store) write(ctx context.Context, fn func(tx *sql.Tx, emit emitFunc) error) error { s.writeMu.Lock() defer s.writeMu.Unlock() tx, err := s.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() events := []api.Event{} emit := func(ev api.Event) error { ev.CreatedAt = time.Now().UTC() payload, err := json.Marshal(ev.Payload) if err != nil { return err } err = tx.QueryRowContext(ctx, ` INSERT INTO events (kind, project_id, task_id, agent_session_id, owner, actor, payload, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING seq `, ev.Kind, ev.ProjectId, ev.TaskId, ev.AgentSessionId, ev.Owner, ev.Actor, string(payload), formatTime(ev.CreatedAt)).Scan(&ev.Seq) if err != nil { return err } events = append(events, ev) return nil } err = fn(tx, emit) if err != nil { return err } touched := []string{} seen := map[string]bool{} for _, ev := range events { if ev.TaskId == nil || seen[*ev.TaskId] { continue } seen[*ev.TaskId] = true touched = append(touched, *ev.TaskId) } for _, taskID := range touched { actor := "byre" for _, ev := range events { if ev.TaskId != nil && *ev.TaskId == taskID { actor = ev.Actor } } err = settleTaskState(ctx, tx, emit, taskID, actor) if err != nil { return err } } err = tx.Commit() if err != nil { return err } s.onEvent(events) return nil } func settleTaskState(ctx context.Context, tx *sql.Tx, emit emitFunc, taskID, actor string) error { projectID := "" before := "" now := "" err := tx.QueryRowContext(ctx, `SELECT project_id, state, `+taskStateExpr+` FROM tasks WHERE id = ?`, taskID).Scan(&projectID, &before, &now) if errors.Is(err, sql.ErrNoRows) { return nil } if err != nil { return err } if before == now { return nil } _, err = tx.ExecContext(ctx, `UPDATE tasks SET state = ? WHERE id = ?`, now, taskID) if err != nil { return err } return emit(api.Event{ Kind: "task.state", ProjectId: &projectID, TaskId: &taskID, Actor: actor, Payload: map[string]any{"from": before, "to": now}, }) } const taskStateExpr = `CASE WHEN tasks.status != 'active' THEN tasks.status WHEN tasks.discard_state != '' THEN 'discarding' WHEN tasks.merging_by != '' OR tasks.finalizing != '' THEN 'merging' ELSE coalesce(( SELECT CASE WHEN s.state != 'idle' THEN s.state WHEN s.subagents > 0 THEN 'running' WHEN EXISTS (SELECT 1 FROM reviews r WHERE r.task_id = tasks.id AND r.state = 'running') THEN 'reviewing' WHEN tasks.review_requests > 0 THEN 'review_pending' ELSE 'idle' END FROM agent_sessions s WHERE s.task_id = tasks.id AND s.role != 'reviewer' AND s.state != 'completed' ORDER BY s.state = 'running' DESC, s.subagents > 0 DESC, s.created_at DESC LIMIT 1 ), 'no_agent') END` func (s *Store) BeginReviewRequest(ctx context.Context, taskID string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { _, err := tx.ExecContext(ctx, `UPDATE tasks SET review_requests = review_requests + 1 WHERE id = ?`, taskID) if err != nil { return err } return settleTaskState(ctx, tx, emit, taskID, "byre") }) } func (s *Store) EndReviewRequest(ctx context.Context, taskID string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { _, err := tx.ExecContext(ctx, `UPDATE tasks SET review_requests = max(review_requests - 1, 0) WHERE id = ?`, taskID) if err != nil { return err } return settleTaskState(ctx, tx, emit, taskID, "byre") }) } func (s *Store) ClearReviewRequests(ctx context.Context) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { rows, err := tx.QueryContext(ctx, `UPDATE tasks SET review_requests = 0 WHERE review_requests > 0 RETURNING id`) if err != nil { return err } ids := []string{} for rows.Next() { id := "" err = rows.Scan(&id) if err != nil { rows.Close() return err } ids = append(ids, id) } rows.Close() err = rows.Err() if err != nil { return err } for _, id := range ids { err = settleTaskState(ctx, tx, emit, id, "byre") if err != nil { return err } } return nil }) } const projectColumns = `id, name, repo_path, default_branch, remote, created_by, created_at` 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, member, created_by, created_at, forked_from, forked_at, subagents` type scanner interface { Scan(dest ...any) error } func scanProject(row scanner) (api.Project, error) { p := api.Project{} createdAt := "" err := row.Scan(&p.Id, &p.Name, &p.RepoPath, &p.DefaultBranch, &p.Remote, &p.CreatedBy, &createdAt) if err != nil { return p, err } p.CreatedAt, err = parseTime(createdAt) return p, err } func scanTask(row scanner) (api.Task, error) { t := api.Task{} status := "" createdAt := "" closedAt := sql.NullString{} mergeCommit := "" todoItem := "" forkedFrom := "" forkedAt := 0 mergeQueuedAt := "" workflowName := "" workflowRun := "" workflowStage := "" parentTaskID := "" parentMergeBy := "" taskState := "" err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &t.Color, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt, &workflowName, &workflowRun, &workflowStage, &parentTaskID, &parentMergeBy, &t.MergeAfterReview, &taskState, &t.Merging, &t.Discarding, &t.SummarySeq) if err != nil { return t, err } t.State = api.TaskState(taskState) if parentTaskID != "" { t.ParentTaskId = &parentTaskID } t.MergingIntoParent = parentMergeBy != "" if workflowName != "" { t.Workflow = &workflowName t.WorkflowRun = &workflowRun t.WorkflowStage = &workflowStage } if t.MergeTitle == "" { t.MergeTitle = t.Title } t.Status = api.TaskStatus(status) t.CreatedAt, err = parseTime(createdAt) if err != nil { return t, err } if closedAt.Valid { ct, err := parseTime(closedAt.String) if err != nil { return t, err } t.ClosedAt = &ct } if mergeCommit != "" { t.MergeCommit = &mergeCommit } if todoItem != "" { t.TodoItem = &todoItem } if forkedFrom != "" { t.ForkedFrom = &forkedFrom t.ForkedAt = &forkedAt } if mergeQueuedAt != "" { qt, err := parseTime(mergeQueuedAt) if err != nil { return t, err } t.MergeQueuedAt = &qt } return t, nil } func scanSession(row scanner) (api.AgentSession, error) { sess := api.AgentSession{} vendor := "" state := "" role := "" createdAt := "" forkedFrom := "" forkedAt := "" 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 } if forkedFrom != "" { sess.ForkedFrom = &forkedFrom sess.ForkedAt = &forkedAt } sess.Vendor = api.AgentSessionVendor(vendor) sess.State = api.AgentSessionState(state) sess.Role = api.AgentSessionRole(role) sess.CreatedAt, err = parseTime(createdAt) return sess, err } func ToPayload(v any) map[string]any { b, err := json.Marshal(v) if err != nil { panic(err) } m := map[string]any{} err = json.Unmarshal(b, &m) if err != nil { panic(err) } return m } func now() string { return formatTime(time.Now()) } const timeLayout = "2006-01-02T15:04:05.000000000Z07:00" func formatTime(t time.Time) string { return t.UTC().Format(timeLayout) } func parseTime(s string) (time.Time, error) { return time.Parse(time.RFC3339Nano, s) }