mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 21:39:54 +02:00
* feat(autopilot): support assigning autopilot to a squad (MUL-2429) Path A (Squad-as-Leader) from the RFC: when an autopilot's assignee is a squad, dispatch resolves to squad.leader_id and executes against the leader's runtime — semantics match a human manually assigning the issue to that squad, no fan-out. Backend scope only; frontend picker change is a follow-up PR. Changes: - 096_autopilot_squad_assignee migration: drop agent FK on autopilot.assignee_id, add assignee_type column (default 'agent'), add autopilot_run.squad_id attribution column. - service.AgentReadiness: single source of truth for archived / runtime-bound / runtime-online checks. Shared by autopilot admission gate, run_only dispatch, and isSquadLeaderReady. - service.resolveAutopilotLeader: translates assignee_type/id to the agent that actually runs the work. - dispatchCreateIssue: stamps issue with assignee_type='squad' for squad autopilots and enqueues via EnqueueTaskForSquadLeader. - dispatchRunOnly: belt-and-braces readiness re-check after resolving squad → leader so a leader that went offline between admission and dispatch produces a clean failure instead of a doomed task. - handler.CreateAutopilot / UpdateAutopilot: accept assignee_type with squad/agent existence + leader-archived validation. Backward-compatible default of "agent" preserves the contract for older clients. - Analytics: AutopilotRunStarted/Completed/Failed events carry assignee_type and squad_id; PostHog can now group autopilot runs by squad without joining back to the autopilot row. Co-authored-by: multica-agent <github@multica.ai> * fix(autopilot): reject archived squads, route post-admission skips, cleanup dangling-agent autopilots (MUL-2429) Addresses three review findings on PR #2888: 1. Archived squad handling: validateAutopilotAssignee now rejects squads with archived_at set; resolveAutopilotLeader returns errSquadArchived so the admission gate fails closed; DeleteSquad now mirrors the issue transfer for autopilot rows (TransferSquadAutopilotsToLeader) so surviving autopilots flip to assignee_type='agent' (leader) instead of dangling at the archived squad. 2. dispatchRunOnly post-admission readiness: introduces errDispatchSkipped sentinel, recognised by DispatchAutopilot via handleDispatchSkip so the run is recorded as `skipped` (not `failed`). Manual triggers no longer 500 when the leader's runtime goes offline between admission and task creation. New TestManualTriggerDoesNotErrorOnPostAdmissionSkip locks the behaviour in. 3. Dangling agent assignee after migration 096 dropped the FK: shouldSkipDispatch now distinguishes pgx.ErrNoRows / errSquadArchived (hard skip — retrying won't help) from transient DB errors (fail-open). DeleteAgentRuntime pauses autopilots that target agents about to be hard-deleted (ListArchivedAgentIDsByRuntime + PauseAutopilotsByAgentAssignees) so the breakage surfaces as a paused row in the UI instead of a quiet skip-burning loop. Unit tests cover the sentinel unwrap contract and errSquadArchived errors.Is behaviour. Integration test TestAutopilotDispatchSkipsWhenRuntimeOffline re-verified against a fresh DB with migration 096 applied. Co-authored-by: multica-agent <github@multica.ai> * fix(autopilot): bump last_run_at on post-admission skip (MUL-2429) Match recordSkippedRun (pre-flight skip) and the success path so the scheduler / "last seen" UI both reflect that this tick evaluated the trigger, even when the post-admission readiness gate caught a late regression. Addresses Emacs review caveat #1 on PR #2888. Co-authored-by: multica-agent <github@multica.ai> * feat(autopilot): mixed agent/squad assignee picker in dialog (MUL-2429) End-to-end UI for assigning an autopilot to a squad. Closes the PR #2888 backend gap: the squad-as-assignee feature was already wired in Go (Path A, RFC §4) but the desktop dialog never offered the choice. - core/types/autopilot: add `AutopilotAssigneeType`, surface `assignee_type` on `Autopilot` + Create/Update request payloads. - views/autopilots/pickers/agent-picker: switch to a polymorphic AssigneeSelection (`{type, id}`); render agents and squads as two grouped sections with shared pinyin search. - views/autopilots/autopilot-dialog: maintain `assigneeType` state, send it on create/update, render the trigger avatar / hover dot with `assignee.type`. - views/autopilots/autopilots-page + autopilot-detail-page: render the assignee row using `autopilot.assignee_type` so squad-typed autopilots show the squad avatar + name, not a broken agent lookup. - locales: add `agents_group` / `squads_group` / `select_assignee` keys (en + zh-Hans), keep legacy `select_agent` for callers that still reference it. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
122 lines
4.4 KiB
SQL
122 lines
4.4 KiB
SQL
-- name: CreateSquad :one
|
||
INSERT INTO squad (workspace_id, name, description, leader_id, creator_id, avatar_url)
|
||
VALUES ($1, $2, $3, $4, $5, $6)
|
||
RETURNING *;
|
||
|
||
-- name: GetSquad :one
|
||
SELECT * FROM squad WHERE id = $1;
|
||
|
||
-- name: GetSquadInWorkspace :one
|
||
SELECT * FROM squad WHERE id = $1 AND workspace_id = $2;
|
||
|
||
-- name: ListSquads :many
|
||
SELECT * FROM squad WHERE workspace_id = $1 AND archived_at IS NULL ORDER BY created_at ASC;
|
||
|
||
-- name: ListAllSquads :many
|
||
SELECT * FROM squad WHERE workspace_id = $1 ORDER BY created_at ASC;
|
||
|
||
-- name: UpdateSquad :one
|
||
UPDATE squad SET
|
||
name = COALESCE(sqlc.narg('name'), name),
|
||
description = COALESCE(sqlc.narg('description'), description),
|
||
leader_id = COALESCE(sqlc.narg('leader_id'), leader_id),
|
||
avatar_url = COALESCE(sqlc.narg('avatar_url'), avatar_url),
|
||
instructions = COALESCE(sqlc.narg('instructions'), instructions),
|
||
updated_at = now()
|
||
WHERE id = $1
|
||
RETURNING *;
|
||
|
||
-- name: ArchiveSquad :one
|
||
UPDATE squad SET archived_at = now(), archived_by = $2, updated_at = now()
|
||
WHERE id = $1
|
||
RETURNING *;
|
||
|
||
-- name: AddSquadMember :one
|
||
INSERT INTO squad_member (squad_id, member_type, member_id, role)
|
||
VALUES ($1, $2, $3, $4)
|
||
RETURNING *;
|
||
|
||
-- name: RemoveSquadMember :execrows
|
||
DELETE FROM squad_member
|
||
WHERE squad_id = $1 AND member_type = $2 AND member_id = $3;
|
||
|
||
-- name: ListSquadMembers :many
|
||
SELECT * FROM squad_member WHERE squad_id = $1 ORDER BY created_at ASC;
|
||
|
||
-- name: UpdateSquadMemberRole :one
|
||
UPDATE squad_member SET role = $4
|
||
WHERE squad_id = $1 AND member_type = $2 AND member_id = $3
|
||
RETURNING *;
|
||
|
||
-- 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;
|
||
|
||
-- name: CountSquadMembers :one
|
||
SELECT count(*) FROM squad_member WHERE squad_id = $1;
|
||
|
||
-- name: GetSquadByAssignee :one
|
||
-- Look up the squad when an issue is assigned to a squad.
|
||
SELECT s.* FROM squad s WHERE s.id = $1 AND s.workspace_id = $2;
|
||
|
||
-- name: ListSquadsByMember :many
|
||
-- Find all squads a given entity belongs to in a workspace.
|
||
SELECT s.* 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;
|
||
|
||
-- name: TransferSquadAssignees :exec
|
||
-- Transfer all issues assigned to a squad to the squad's leader agent.
|
||
UPDATE issue SET assignee_type = 'agent', assignee_id = $2, updated_at = now()
|
||
WHERE assignee_type = 'squad' AND assignee_id = $1;
|
||
|
||
-- name: TransferSquadAutopilotsToLeader :exec
|
||
-- 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).
|
||
UPDATE autopilot
|
||
SET assignee_type = 'agent',
|
||
assignee_id = $2,
|
||
updated_at = now()
|
||
WHERE assignee_type = 'squad' AND assignee_id = $1;
|
||
|
||
-- name: ListSquadMemberStatusRows :many
|
||
-- 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.
|
||
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')
|
||
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;
|