Files
byre/internal/store/landing.go
T

115 lines
3.8 KiB
Go

package store
import (
"context"
"database/sql"
"errors"
"fmt"
"g.fc.run/ai/byre/internal/api"
)
func (s *Store) StartLanding(ctx context.Context, taskID, sessionID, login string) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
landing := ""
running := false
err := tx.QueryRowContext(ctx, `SELECT landing_session, `+implementerRunning+` FROM tasks WHERE id = ?`, taskID).Scan(&landing, &running)
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
}
if err != nil {
return err
}
switch {
case landing != "":
return fmt.Errorf("%w: task %s is already merging and continuing", ErrStateChanged, taskID)
case running:
return fmt.Errorf("%w: an agent on task %s is mid-turn; merge when they're all idle", ErrStateChanged, taskID)
}
idle := api.AgentSessionStateIdle
err = transitionSessionTx(ctx, tx, emit, sessionID, &idle, api.AgentSessionStateRunning, "byre", nil)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `UPDATE tasks SET landing_session = ?, landing_by = ? WHERE id = ?`, sessionID, login, taskID)
return err
})
}
func (s *Store) LandingBy(ctx context.Context, taskID, sessionID string) (string, error) {
by := ""
err := s.db.QueryRowContext(ctx, `SELECT landing_by FROM tasks WHERE id = ? AND landing_session = ?`, taskID, sessionID).Scan(&by)
if errors.Is(err, sql.ErrNoRows) {
return "", nil
}
return by, err
}
func (s *Store) HeldForLanding(ctx context.Context, taskID, sessionID string) (bool, error) {
held := false
err := s.db.QueryRowContext(ctx, `SELECT landing_session != '' AND landing_session != ? FROM tasks WHERE id = ?`, sessionID, taskID).Scan(&held)
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
return held, err
}
func endLandings(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `UPDATE tasks SET landing_session = '', landing_by = ''
WHERE landing_session != '' AND NOT EXISTS (SELECT 1 FROM agent_sessions s WHERE s.id = tasks.landing_session AND s.state = 'running')`)
return err
}
type QueuedDelivery struct {
SessionID string
Input int64
Response string
}
func queuedDeliveries(ctx context.Context, tx *sql.Tx) ([]QueuedDelivery, error) {
rows, err := tx.QueryContext(ctx, `
SELECT s.id, s.task_id,
coalesce((SELECT min(q.id) FROM task_inputs q WHERE q.task_id = s.task_id AND q.member = s.member), 0),
coalesce((SELECT r.id FROM inbox_responses r JOIN inbox_items i ON i.id = r.item_id
WHERE i.agent_session_id = s.id AND r.delivered_at IS NULL ORDER BY r.created_at LIMIT 1), '')
FROM agent_sessions s JOIN tasks t ON t.id = s.task_id
WHERE s.role = 'implementer' AND s.state = 'idle'
AND t.status = 'active' AND t.discard_state = '' AND t.merging_by = '' AND t.finalizing = '' AND t.landing_session = ''
AND (EXISTS (SELECT 1 FROM task_inputs q WHERE q.task_id = s.task_id AND q.member = s.member)
OR EXISTS (SELECT 1 FROM inbox_responses r JOIN inbox_items i ON i.id = r.item_id WHERE i.agent_session_id = s.id AND r.delivered_at IS NULL))
AND s.id = (SELECT l.id FROM agent_sessions l WHERE l.task_id = s.task_id AND l.member = s.member AND l.role = 'implementer' AND l.state != 'completed' ORDER BY l.created_at DESC LIMIT 1)`)
if err != nil {
return nil, err
}
type candidate struct {
QueuedDelivery
taskID string
}
found := []candidate{}
for rows.Next() {
c := candidate{}
err = rows.Scan(&c.SessionID, &c.taskID, &c.Input, &c.Response)
if err != nil {
rows.Close()
return nil, err
}
found = append(found, c)
}
err = errors.Join(rows.Err(), rows.Close())
if err != nil {
return nil, err
}
deliveries := []QueuedDelivery{}
for _, c := range found {
stopped, err := sessionStopped(ctx, tx, c.taskID, c.SessionID)
if err != nil {
return nil, err
}
if !stopped {
deliveries = append(deliveries, c.QueuedDelivery)
}
}
return deliveries, nil
}