mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-06 10:50:54 +02:00
* feat(runtime): unbind agents on runtime delete instead of destroying them Deleting a runtime archived its agents and then hard-deleted the rows, so the agents and every conversation with them disappeared — while the confirmation dialog said "archive", which a user reasonably reads as recoverable. Retiring a laptop is an ordinary action; losing the agents configured on it is not an ordinary consequence. An agent is now a persistent business object and a runtime is replaceable execution capacity: deleting a runtime unbinds its agents. `runtime_id IS NULL` means unbound — orthogonal to archived — and the agent keeps its instructions, skills, chats, labels, channel installations, autopilots and task history. service.AgentReadiness already refused an agent with no runtime, so the scheduling safety gate needed no change. Two columns become nullable, not one. Without `agent_task_queue.runtime_id`, deleting the runtime still cascades the task history away (and task_message / task_usage / task_token with it), so the agents would survive with no record of anything they did — the same class of loss. A NOT VALID CHECK keeps NULL confined to history: an active task must always have a runtime, so claim / dispatch / delivery-CAS paths can never observe one without. It is written against completed_at rather than a status list so a future non-terminal status fails closed instead of slipping through. Two prerequisites this depends on: - 'deferred' (migration 128) was missing from CancelAgentTasksByRuntimeOrAgent. It went unnoticed because the delete used to cascade those rows away; with the new CHECK it would abort the delete and make the runtime undeletable. - The channel-installation / label / chat-pin / invocation-target / draft-restore cleanups were scoped to "archived agents on this runtime". Archived user agents now survive, so that scope is narrowed to kind='system' — otherwise the fix would produce a subtler loss: agent alive, configuration wiped. Also removes the squad guard that refused (409) when an active squad's leader was an archived agent on the runtime, plus the archived-squad delete that existed only to get past squad.leader_id's RESTRICT FK. The leader is no longer deleted, so nothing needs to be given up to retire a machine. Autopilots are no longer paused either: their assignee survives, and a rebind restores them without the owner having to remember to re-enable. Reason codes: an unbound agent reports agent_runtime_required, not runtime_offline. The copy for runtime_offline tells users to reconnect a machine; an unbound agent has no machine to reconnect, and the fix is to bind a runtime. Chat's bare 409 string gains the same code so the composer can offer that action. API: agents gain runtime_bound. runtime_id stays a string (empty when unbound) so installed clients keep parsing and no gated two-release rollout is needed. The confirmed-delete endpoint is /unbind-agents-and-delete; /archive-agents-and-delete still routes to it, and the compared expected_active_agent_ids set is unchanged — widening it would 409 every older client forever. Co-authored-by: multica-agent <github@multica.ai> * fix: make runtime unbinding recoverable Co-authored-by: multica-agent <github@multica.ai> * fix: address runtime unbind review nits Co-authored-by: multica-agent <github@multica.ai> * fix: resolve runtime unbind review blockers Co-authored-by: multica-agent <github@multica.ai> * fix(migrations): renumber runtime unbind after main merge Co-authored-by: multica-agent <github@multica.ai> * test(daemon): avoid late-request lease flake Co-authored-by: multica-agent <github@multica.ai> * test(autopilots): bind validation fixture runtime Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>
773 lines
21 KiB
Go
773 lines
21 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
||
// versions:
|
||
// sqlc v1.31.1
|
||
// source: squad.sql
|
||
|
||
package db
|
||
|
||
import (
|
||
"context"
|
||
|
||
"github.com/jackc/pgx/v5/pgtype"
|
||
)
|
||
|
||
const addSquadMember = `-- name: AddSquadMember :one
|
||
INSERT INTO squad_member (squad_id, member_type, member_id, role)
|
||
VALUES ($1, $2, $3, $4)
|
||
RETURNING id, squad_id, member_type, member_id, role, created_at
|
||
`
|
||
|
||
type AddSquadMemberParams struct {
|
||
SquadID pgtype.UUID `json:"squad_id"`
|
||
MemberType string `json:"member_type"`
|
||
MemberID pgtype.UUID `json:"member_id"`
|
||
Role string `json:"role"`
|
||
}
|
||
|
||
func (q *Queries) AddSquadMember(ctx context.Context, arg AddSquadMemberParams) (SquadMember, error) {
|
||
row := q.db.QueryRow(ctx, addSquadMember,
|
||
arg.SquadID,
|
||
arg.MemberType,
|
||
arg.MemberID,
|
||
arg.Role,
|
||
)
|
||
var i SquadMember
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.SquadID,
|
||
&i.MemberType,
|
||
&i.MemberID,
|
||
&i.Role,
|
||
&i.CreatedAt,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const archiveSquad = `-- name: ArchiveSquad :one
|
||
UPDATE squad SET archived_at = now(), archived_by = $2, updated_at = now()
|
||
WHERE id = $1
|
||
RETURNING id, workspace_id, name, description, leader_id, creator_id, created_at, updated_at, archived_at, archived_by, avatar_url, instructions
|
||
`
|
||
|
||
type ArchiveSquadParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
ArchivedBy pgtype.UUID `json:"archived_by"`
|
||
}
|
||
|
||
func (q *Queries) ArchiveSquad(ctx context.Context, arg ArchiveSquadParams) (Squad, error) {
|
||
row := q.db.QueryRow(ctx, archiveSquad, arg.ID, arg.ArchivedBy)
|
||
var i Squad
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.Description,
|
||
&i.LeaderID,
|
||
&i.CreatorID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.AvatarUrl,
|
||
&i.Instructions,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const countSquadMembers = `-- name: CountSquadMembers :one
|
||
SELECT count(*) FROM squad_member WHERE squad_id = $1
|
||
`
|
||
|
||
func (q *Queries) CountSquadMembers(ctx context.Context, squadID pgtype.UUID) (int64, error) {
|
||
row := q.db.QueryRow(ctx, countSquadMembers, squadID)
|
||
var count int64
|
||
err := row.Scan(&count)
|
||
return count, err
|
||
}
|
||
|
||
const createSquad = `-- name: CreateSquad :one
|
||
INSERT INTO squad (workspace_id, name, description, leader_id, creator_id, avatar_url)
|
||
VALUES ($1, $2, $3, $4, $5, $6)
|
||
RETURNING id, workspace_id, name, description, leader_id, creator_id, created_at, updated_at, archived_at, archived_by, avatar_url, instructions
|
||
`
|
||
|
||
type CreateSquadParams struct {
|
||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||
Name string `json:"name"`
|
||
Description string `json:"description"`
|
||
LeaderID pgtype.UUID `json:"leader_id"`
|
||
CreatorID pgtype.UUID `json:"creator_id"`
|
||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||
}
|
||
|
||
func (q *Queries) CreateSquad(ctx context.Context, arg CreateSquadParams) (Squad, error) {
|
||
row := q.db.QueryRow(ctx, createSquad,
|
||
arg.WorkspaceID,
|
||
arg.Name,
|
||
arg.Description,
|
||
arg.LeaderID,
|
||
arg.CreatorID,
|
||
arg.AvatarUrl,
|
||
)
|
||
var i Squad
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.Description,
|
||
&i.LeaderID,
|
||
&i.CreatorID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.AvatarUrl,
|
||
&i.Instructions,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const getSquad = `-- name: GetSquad :one
|
||
SELECT id, workspace_id, name, description, leader_id, creator_id, created_at, updated_at, archived_at, archived_by, avatar_url, instructions FROM squad WHERE id = $1
|
||
`
|
||
|
||
func (q *Queries) GetSquad(ctx context.Context, id pgtype.UUID) (Squad, error) {
|
||
row := q.db.QueryRow(ctx, getSquad, id)
|
||
var i Squad
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.Description,
|
||
&i.LeaderID,
|
||
&i.CreatorID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.AvatarUrl,
|
||
&i.Instructions,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const getSquadByAssignee = `-- name: GetSquadByAssignee :one
|
||
SELECT s.id, s.workspace_id, s.name, s.description, s.leader_id, s.creator_id, s.created_at, s.updated_at, s.archived_at, s.archived_by, s.avatar_url, s.instructions FROM squad s WHERE s.id = $1 AND s.workspace_id = $2
|
||
`
|
||
|
||
type GetSquadByAssigneeParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||
}
|
||
|
||
// Look up the squad when an issue is assigned to a squad.
|
||
func (q *Queries) GetSquadByAssignee(ctx context.Context, arg GetSquadByAssigneeParams) (Squad, error) {
|
||
row := q.db.QueryRow(ctx, getSquadByAssignee, arg.ID, arg.WorkspaceID)
|
||
var i Squad
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.Description,
|
||
&i.LeaderID,
|
||
&i.CreatorID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.AvatarUrl,
|
||
&i.Instructions,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const getSquadInWorkspace = `-- name: GetSquadInWorkspace :one
|
||
SELECT id, workspace_id, name, description, leader_id, creator_id, created_at, updated_at, archived_at, archived_by, avatar_url, instructions FROM squad WHERE id = $1 AND workspace_id = $2
|
||
`
|
||
|
||
type GetSquadInWorkspaceParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||
}
|
||
|
||
func (q *Queries) GetSquadInWorkspace(ctx context.Context, arg GetSquadInWorkspaceParams) (Squad, error) {
|
||
row := q.db.QueryRow(ctx, getSquadInWorkspace, arg.ID, arg.WorkspaceID)
|
||
var i Squad
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.Description,
|
||
&i.LeaderID,
|
||
&i.CreatorID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.AvatarUrl,
|
||
&i.Instructions,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const isSquadMember = `-- name: IsSquadMember :one
|
||
SELECT EXISTS(
|
||
SELECT 1 FROM squad_member
|
||
WHERE squad_id = $1 AND member_type = $2 AND member_id = $3
|
||
) AS is_member
|
||
`
|
||
|
||
type IsSquadMemberParams struct {
|
||
SquadID pgtype.UUID `json:"squad_id"`
|
||
MemberType string `json:"member_type"`
|
||
MemberID pgtype.UUID `json:"member_id"`
|
||
}
|
||
|
||
func (q *Queries) IsSquadMember(ctx context.Context, arg IsSquadMemberParams) (bool, error) {
|
||
row := q.db.QueryRow(ctx, isSquadMember, arg.SquadID, arg.MemberType, arg.MemberID)
|
||
var is_member bool
|
||
err := row.Scan(&is_member)
|
||
return is_member, err
|
||
}
|
||
|
||
const listAllSquads = `-- name: ListAllSquads :many
|
||
SELECT id, workspace_id, name, description, leader_id, creator_id, created_at, updated_at, archived_at, archived_by, avatar_url, instructions FROM squad WHERE workspace_id = $1 ORDER BY created_at ASC
|
||
`
|
||
|
||
func (q *Queries) ListAllSquads(ctx context.Context, workspaceID pgtype.UUID) ([]Squad, error) {
|
||
rows, err := q.db.Query(ctx, listAllSquads, workspaceID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []Squad{}
|
||
for rows.Next() {
|
||
var i Squad
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.Description,
|
||
&i.LeaderID,
|
||
&i.CreatorID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.AvatarUrl,
|
||
&i.Instructions,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listSquadMemberPreviewRows = `-- name: ListSquadMemberPreviewRows :many
|
||
SELECT
|
||
sm.squad_id,
|
||
sm.member_type,
|
||
sm.member_id,
|
||
sm.role
|
||
FROM squad_member sm
|
||
JOIN squad s ON s.id = sm.squad_id
|
||
WHERE s.workspace_id = $1 AND s.archived_at IS NULL
|
||
ORDER BY
|
||
sm.squad_id ASC,
|
||
(sm.member_type = 'agent' AND sm.member_id = s.leader_id) DESC,
|
||
sm.created_at ASC
|
||
`
|
||
|
||
type ListSquadMemberPreviewRowsRow struct {
|
||
SquadID pgtype.UUID `json:"squad_id"`
|
||
MemberType string `json:"member_type"`
|
||
MemberID pgtype.UUID `json:"member_id"`
|
||
Role string `json:"role"`
|
||
}
|
||
|
||
// Static squad membership summary for list/hover previews. This deliberately
|
||
// excludes derived runtime/task status; the squad detail members-status
|
||
// endpoint owns live state.
|
||
func (q *Queries) ListSquadMemberPreviewRows(ctx context.Context, workspaceID pgtype.UUID) ([]ListSquadMemberPreviewRowsRow, error) {
|
||
rows, err := q.db.Query(ctx, listSquadMemberPreviewRows, workspaceID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []ListSquadMemberPreviewRowsRow{}
|
||
for rows.Next() {
|
||
var i ListSquadMemberPreviewRowsRow
|
||
if err := rows.Scan(
|
||
&i.SquadID,
|
||
&i.MemberType,
|
||
&i.MemberID,
|
||
&i.Role,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listSquadMemberPreviewRowsBySquad = `-- name: ListSquadMemberPreviewRowsBySquad :many
|
||
SELECT
|
||
sm.squad_id,
|
||
sm.member_type,
|
||
sm.member_id,
|
||
sm.role
|
||
FROM squad_member sm
|
||
JOIN squad s ON s.id = sm.squad_id
|
||
WHERE sm.squad_id = $1
|
||
ORDER BY
|
||
(sm.member_type = 'agent' AND sm.member_id = s.leader_id) DESC,
|
||
sm.created_at ASC
|
||
`
|
||
|
||
type ListSquadMemberPreviewRowsBySquadRow struct {
|
||
SquadID pgtype.UUID `json:"squad_id"`
|
||
MemberType string `json:"member_type"`
|
||
MemberID pgtype.UUID `json:"member_id"`
|
||
Role string `json:"role"`
|
||
}
|
||
|
||
func (q *Queries) ListSquadMemberPreviewRowsBySquad(ctx context.Context, squadID pgtype.UUID) ([]ListSquadMemberPreviewRowsBySquadRow, error) {
|
||
rows, err := q.db.Query(ctx, listSquadMemberPreviewRowsBySquad, squadID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []ListSquadMemberPreviewRowsBySquadRow{}
|
||
for rows.Next() {
|
||
var i ListSquadMemberPreviewRowsBySquadRow
|
||
if err := rows.Scan(
|
||
&i.SquadID,
|
||
&i.MemberType,
|
||
&i.MemberID,
|
||
&i.Role,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listSquadMemberStatusRows = `-- name: ListSquadMemberStatusRows :many
|
||
SELECT
|
||
sm.id AS squad_member_id,
|
||
sm.member_type AS member_type,
|
||
sm.member_id AS member_id,
|
||
a.archived_at AS agent_archived_at,
|
||
ar.status AS runtime_status,
|
||
ar.last_seen_at AS runtime_last_seen_at,
|
||
atq.id AS task_id,
|
||
atq.status AS task_status,
|
||
atq.issue_id AS task_issue_id,
|
||
atq.dispatched_at AS task_dispatched_at,
|
||
i.number AS issue_number,
|
||
i.title AS issue_title,
|
||
i.status AS issue_status
|
||
FROM squad_member sm
|
||
LEFT JOIN agent a
|
||
ON sm.member_type = 'agent' AND a.id = sm.member_id
|
||
LEFT JOIN agent_runtime ar
|
||
ON ar.id = a.runtime_id
|
||
LEFT JOIN agent_task_queue atq
|
||
ON sm.member_type = 'agent'
|
||
AND atq.agent_id = sm.member_id
|
||
AND atq.status IN ('dispatched', 'running', 'waiting_local_directory')
|
||
LEFT JOIN issue i
|
||
ON i.id = atq.issue_id
|
||
WHERE sm.squad_id = $1
|
||
ORDER BY sm.created_at ASC, atq.dispatched_at DESC NULLS LAST
|
||
`
|
||
|
||
type ListSquadMemberStatusRowsRow struct {
|
||
SquadMemberID pgtype.UUID `json:"squad_member_id"`
|
||
MemberType string `json:"member_type"`
|
||
MemberID pgtype.UUID `json:"member_id"`
|
||
AgentArchivedAt pgtype.Timestamptz `json:"agent_archived_at"`
|
||
RuntimeStatus pgtype.Text `json:"runtime_status"`
|
||
RuntimeLastSeenAt pgtype.Timestamptz `json:"runtime_last_seen_at"`
|
||
TaskID pgtype.UUID `json:"task_id"`
|
||
TaskStatus pgtype.Text `json:"task_status"`
|
||
TaskIssueID pgtype.UUID `json:"task_issue_id"`
|
||
TaskDispatchedAt pgtype.Timestamptz `json:"task_dispatched_at"`
|
||
IssueNumber pgtype.Int4 `json:"issue_number"`
|
||
IssueTitle pgtype.Text `json:"issue_title"`
|
||
IssueStatus pgtype.Text `json:"issue_status"`
|
||
}
|
||
|
||
// Per-row join used to build the squad-members status view. One row per
|
||
// (squad_member × active_task); members with no active task return a
|
||
// single row with NULL task_* columns. Human members and agent members
|
||
// with no agent row also return one row with NULL agent_/runtime_ columns.
|
||
// The handler aggregates rows by member_id.
|
||
func (q *Queries) ListSquadMemberStatusRows(ctx context.Context, squadID pgtype.UUID) ([]ListSquadMemberStatusRowsRow, error) {
|
||
rows, err := q.db.Query(ctx, listSquadMemberStatusRows, squadID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []ListSquadMemberStatusRowsRow{}
|
||
for rows.Next() {
|
||
var i ListSquadMemberStatusRowsRow
|
||
if err := rows.Scan(
|
||
&i.SquadMemberID,
|
||
&i.MemberType,
|
||
&i.MemberID,
|
||
&i.AgentArchivedAt,
|
||
&i.RuntimeStatus,
|
||
&i.RuntimeLastSeenAt,
|
||
&i.TaskID,
|
||
&i.TaskStatus,
|
||
&i.TaskIssueID,
|
||
&i.TaskDispatchedAt,
|
||
&i.IssueNumber,
|
||
&i.IssueTitle,
|
||
&i.IssueStatus,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listSquadMembers = `-- name: ListSquadMembers :many
|
||
SELECT id, squad_id, member_type, member_id, role, created_at FROM squad_member WHERE squad_id = $1 ORDER BY created_at ASC
|
||
`
|
||
|
||
func (q *Queries) ListSquadMembers(ctx context.Context, squadID pgtype.UUID) ([]SquadMember, error) {
|
||
rows, err := q.db.Query(ctx, listSquadMembers, squadID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []SquadMember{}
|
||
for rows.Next() {
|
||
var i SquadMember
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.SquadID,
|
||
&i.MemberType,
|
||
&i.MemberID,
|
||
&i.Role,
|
||
&i.CreatedAt,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listSquads = `-- name: ListSquads :many
|
||
SELECT id, workspace_id, name, description, leader_id, creator_id, created_at, updated_at, archived_at, archived_by, avatar_url, instructions FROM squad WHERE workspace_id = $1 AND archived_at IS NULL ORDER BY created_at ASC
|
||
`
|
||
|
||
func (q *Queries) ListSquads(ctx context.Context, workspaceID pgtype.UUID) ([]Squad, error) {
|
||
rows, err := q.db.Query(ctx, listSquads, workspaceID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []Squad{}
|
||
for rows.Next() {
|
||
var i Squad
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.Description,
|
||
&i.LeaderID,
|
||
&i.CreatorID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.AvatarUrl,
|
||
&i.Instructions,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listSquadsByMember = `-- name: ListSquadsByMember :many
|
||
SELECT s.id, s.workspace_id, s.name, s.description, s.leader_id, s.creator_id, s.created_at, s.updated_at, s.archived_at, s.archived_by, s.avatar_url, s.instructions FROM squad s
|
||
JOIN squad_member sm ON sm.squad_id = s.id
|
||
WHERE s.workspace_id = $1 AND sm.member_type = $2 AND sm.member_id = $3
|
||
ORDER BY s.created_at ASC
|
||
`
|
||
|
||
type ListSquadsByMemberParams struct {
|
||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||
MemberType string `json:"member_type"`
|
||
MemberID pgtype.UUID `json:"member_id"`
|
||
}
|
||
|
||
// Find all squads a given entity belongs to in a workspace.
|
||
func (q *Queries) ListSquadsByMember(ctx context.Context, arg ListSquadsByMemberParams) ([]Squad, error) {
|
||
rows, err := q.db.Query(ctx, listSquadsByMember, arg.WorkspaceID, arg.MemberType, arg.MemberID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []Squad{}
|
||
for rows.Next() {
|
||
var i Squad
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.Description,
|
||
&i.LeaderID,
|
||
&i.CreatorID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.AvatarUrl,
|
||
&i.Instructions,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const lockSquadForAutopilotAssignment = `-- name: LockSquadForAutopilotAssignment :one
|
||
SELECT id, workspace_id, name, description, leader_id, creator_id, created_at, updated_at, archived_at, archived_by, avatar_url, instructions FROM squad
|
||
WHERE id = $1 AND workspace_id = $2
|
||
FOR SHARE
|
||
`
|
||
|
||
type LockSquadForAutopilotAssignmentParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||
}
|
||
|
||
// Stabilizes the squad-to-leader resolution while an active Autopilot is
|
||
// created, retargeted, or resumed. FOR SHARE conflicts with an ordinary
|
||
// leader_id update, so the caller subsequently locks the same leader Agent
|
||
// whose row Runtime teardown serializes against.
|
||
func (q *Queries) LockSquadForAutopilotAssignment(ctx context.Context, arg LockSquadForAutopilotAssignmentParams) (Squad, error) {
|
||
row := q.db.QueryRow(ctx, lockSquadForAutopilotAssignment, arg.ID, arg.WorkspaceID)
|
||
var i Squad
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.Description,
|
||
&i.LeaderID,
|
||
&i.CreatorID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.AvatarUrl,
|
||
&i.Instructions,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const lockSquadForUpdate = `-- name: LockSquadForUpdate :one
|
||
SELECT id, workspace_id, name, description, leader_id, creator_id, created_at, updated_at, archived_at, archived_by, avatar_url, instructions FROM squad
|
||
WHERE id = $1 AND workspace_id = $2
|
||
FOR UPDATE
|
||
`
|
||
|
||
type LockSquadForUpdateParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||
}
|
||
|
||
// Squad leader changes take the exclusive side of the same lock used by
|
||
// Autopilot assignment. The handler then locks the proposed leader Agent and
|
||
// pauses active squad Autopilots when that Agent is unbound.
|
||
func (q *Queries) LockSquadForUpdate(ctx context.Context, arg LockSquadForUpdateParams) (Squad, error) {
|
||
row := q.db.QueryRow(ctx, lockSquadForUpdate, arg.ID, arg.WorkspaceID)
|
||
var i Squad
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.Description,
|
||
&i.LeaderID,
|
||
&i.CreatorID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.AvatarUrl,
|
||
&i.Instructions,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const removeSquadMember = `-- name: RemoveSquadMember :execrows
|
||
DELETE FROM squad_member
|
||
WHERE squad_id = $1 AND member_type = $2 AND member_id = $3
|
||
`
|
||
|
||
type RemoveSquadMemberParams struct {
|
||
SquadID pgtype.UUID `json:"squad_id"`
|
||
MemberType string `json:"member_type"`
|
||
MemberID pgtype.UUID `json:"member_id"`
|
||
}
|
||
|
||
func (q *Queries) RemoveSquadMember(ctx context.Context, arg RemoveSquadMemberParams) (int64, error) {
|
||
result, err := q.db.Exec(ctx, removeSquadMember, arg.SquadID, arg.MemberType, arg.MemberID)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return result.RowsAffected(), nil
|
||
}
|
||
|
||
const transferSquadAssignees = `-- name: TransferSquadAssignees :exec
|
||
UPDATE issue SET assignee_type = 'agent', assignee_id = $2, updated_at = now()
|
||
WHERE assignee_type = 'squad' AND assignee_id = $1
|
||
`
|
||
|
||
type TransferSquadAssigneesParams struct {
|
||
AssigneeID pgtype.UUID `json:"assignee_id"`
|
||
AssigneeID_2 pgtype.UUID `json:"assignee_id_2"`
|
||
}
|
||
|
||
// Transfer all issues assigned to a squad to the squad's leader agent.
|
||
func (q *Queries) TransferSquadAssignees(ctx context.Context, arg TransferSquadAssigneesParams) error {
|
||
_, err := q.db.Exec(ctx, transferSquadAssignees, arg.AssigneeID, arg.AssigneeID_2)
|
||
return err
|
||
}
|
||
|
||
const transferSquadAutopilotsToLeader = `-- name: TransferSquadAutopilotsToLeader :exec
|
||
UPDATE autopilot
|
||
SET assignee_type = 'agent',
|
||
assignee_id = $2,
|
||
updated_at = now()
|
||
WHERE assignee_type = 'squad' AND assignee_id = $1
|
||
`
|
||
|
||
type TransferSquadAutopilotsToLeaderParams struct {
|
||
AssigneeID pgtype.UUID `json:"assignee_id"`
|
||
AssigneeID_2 pgtype.UUID `json:"assignee_id_2"`
|
||
}
|
||
|
||
// Mirrors TransferSquadAssignees for autopilot rows: when a squad is archived,
|
||
// any autopilot still pointing at the squad would otherwise dangle and the
|
||
// admission gate would skip every subsequent dispatch with "assignee squad
|
||
// cannot be resolved". Rewrite the assignee in place to the leader agent so
|
||
// the autopilot keeps firing under the same leader-only execution semantics
|
||
// it had a moment before the archive (Path A from MUL-2429).
|
||
func (q *Queries) TransferSquadAutopilotsToLeader(ctx context.Context, arg TransferSquadAutopilotsToLeaderParams) error {
|
||
_, err := q.db.Exec(ctx, transferSquadAutopilotsToLeader, arg.AssigneeID, arg.AssigneeID_2)
|
||
return err
|
||
}
|
||
|
||
const updateSquad = `-- name: UpdateSquad :one
|
||
UPDATE squad SET
|
||
name = COALESCE($2, name),
|
||
description = COALESCE($3, description),
|
||
leader_id = COALESCE($4, leader_id),
|
||
avatar_url = COALESCE($5, avatar_url),
|
||
instructions = COALESCE($6, instructions),
|
||
updated_at = now()
|
||
WHERE id = $1
|
||
RETURNING id, workspace_id, name, description, leader_id, creator_id, created_at, updated_at, archived_at, archived_by, avatar_url, instructions
|
||
`
|
||
|
||
type UpdateSquadParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
Name pgtype.Text `json:"name"`
|
||
Description pgtype.Text `json:"description"`
|
||
LeaderID pgtype.UUID `json:"leader_id"`
|
||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||
Instructions pgtype.Text `json:"instructions"`
|
||
}
|
||
|
||
func (q *Queries) UpdateSquad(ctx context.Context, arg UpdateSquadParams) (Squad, error) {
|
||
row := q.db.QueryRow(ctx, updateSquad,
|
||
arg.ID,
|
||
arg.Name,
|
||
arg.Description,
|
||
arg.LeaderID,
|
||
arg.AvatarUrl,
|
||
arg.Instructions,
|
||
)
|
||
var i Squad
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.Description,
|
||
&i.LeaderID,
|
||
&i.CreatorID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.AvatarUrl,
|
||
&i.Instructions,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const updateSquadMemberRole = `-- name: UpdateSquadMemberRole :one
|
||
UPDATE squad_member SET role = $4
|
||
WHERE squad_id = $1 AND member_type = $2 AND member_id = $3
|
||
RETURNING id, squad_id, member_type, member_id, role, created_at
|
||
`
|
||
|
||
type UpdateSquadMemberRoleParams struct {
|
||
SquadID pgtype.UUID `json:"squad_id"`
|
||
MemberType string `json:"member_type"`
|
||
MemberID pgtype.UUID `json:"member_id"`
|
||
Role string `json:"role"`
|
||
}
|
||
|
||
func (q *Queries) UpdateSquadMemberRole(ctx context.Context, arg UpdateSquadMemberRoleParams) (SquadMember, error) {
|
||
row := q.db.QueryRow(ctx, updateSquadMemberRole,
|
||
arg.SquadID,
|
||
arg.MemberType,
|
||
arg.MemberID,
|
||
arg.Role,
|
||
)
|
||
var i SquadMember
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.SquadID,
|
||
&i.MemberType,
|
||
&i.MemberID,
|
||
&i.Role,
|
||
&i.CreatedAt,
|
||
)
|
||
return i, err
|
||
}
|