mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-05 01:19:42 +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>
171 lines
5.7 KiB
Go
171 lines
5.7 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: agent_invocation_target.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const createAgentInvocationTarget = `-- name: CreateAgentInvocationTarget :exec
|
|
INSERT INTO agent_invocation_target (agent_id, target_type, target_id, created_by)
|
|
VALUES ($1, $2, $3, $4)
|
|
ON CONFLICT (agent_id, target_type, target_id) DO UPDATE SET
|
|
created_by = EXCLUDED.created_by,
|
|
created_at = now()
|
|
`
|
|
|
|
type CreateAgentInvocationTargetParams struct {
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
TargetType string `json:"target_type"`
|
|
TargetID pgtype.UUID `json:"target_id"`
|
|
CreatedBy pgtype.UUID `json:"created_by"`
|
|
}
|
|
|
|
// Idempotent upsert: re-adding an existing (agent, target_type, target)
|
|
// refreshes created_by/created_at rather than erroring. Callers replace the
|
|
// whole set via DeleteAgentInvocationTargets + a series of these, so the
|
|
// ON CONFLICT is belt-and-suspenders against races.
|
|
func (q *Queries) CreateAgentInvocationTarget(ctx context.Context, arg CreateAgentInvocationTargetParams) error {
|
|
_, err := q.db.Exec(ctx, createAgentInvocationTarget,
|
|
arg.AgentID,
|
|
arg.TargetType,
|
|
arg.TargetID,
|
|
arg.CreatedBy,
|
|
)
|
|
return err
|
|
}
|
|
|
|
const deleteAgentInvocationTargets = `-- name: DeleteAgentInvocationTargets :exec
|
|
DELETE FROM agent_invocation_target
|
|
WHERE agent_id = $1
|
|
`
|
|
|
|
// Clears every target for an agent. Used before re-writing the allow-list so
|
|
// the update is a wholesale replace, matching the composio_toolkit_allowlist
|
|
// write model.
|
|
func (q *Queries) DeleteAgentInvocationTargets(ctx context.Context, agentID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteAgentInvocationTargets, agentID)
|
|
return err
|
|
}
|
|
|
|
const deleteAgentInvocationTargetsByMember = `-- name: DeleteAgentInvocationTargetsByMember :exec
|
|
DELETE FROM agent_invocation_target ait
|
|
USING agent a
|
|
WHERE ait.agent_id = a.id
|
|
AND a.workspace_id = $1
|
|
AND ait.target_type = 'member'
|
|
AND ait.target_id = $2
|
|
`
|
|
|
|
type DeleteAgentInvocationTargetsByMemberParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
TargetID pgtype.UUID `json:"target_id"`
|
|
}
|
|
|
|
// Removes member-target grants pointing at a leaving user, SCOPED to a single
|
|
// workspace. A user may belong to multiple workspaces; removing them from one
|
|
// must NOT touch their invocation grants on agents in another workspace. Joins
|
|
// through agent (agent_invocation_target has no workspace_id column and no FK)
|
|
// to bound the delete to @workspace_id.
|
|
func (q *Queries) DeleteAgentInvocationTargetsByMember(ctx context.Context, arg DeleteAgentInvocationTargetsByMemberParams) error {
|
|
_, err := q.db.Exec(ctx, deleteAgentInvocationTargetsByMember, arg.WorkspaceID, arg.TargetID)
|
|
return err
|
|
}
|
|
|
|
const deleteAgentInvocationTargetsBySystemRuntimeAgents = `-- name: DeleteAgentInvocationTargetsBySystemRuntimeAgents :exec
|
|
DELETE FROM agent_invocation_target
|
|
WHERE agent_id IN (
|
|
SELECT id FROM agent WHERE runtime_id = $1 AND kind = 'system'
|
|
)
|
|
`
|
|
|
|
// Application-layer replacement for the (deliberately absent) agent_id ON
|
|
// DELETE CASCADE: removes invocation targets for the system agents a runtime
|
|
// delete is about to hard-delete. MUST run in the same tx as, and BEFORE,
|
|
// DeleteSystemAgentsByRuntime so no orphan target rows survive the agent rows
|
|
// they belonged to. Mirrors the agent hard-delete predicate exactly.
|
|
//
|
|
// Scoped to kind = 'system' since MUL-5559: user agents are no longer deleted
|
|
// with their runtime (they are unbound and keep their configuration), so
|
|
// clearing THEIR invocation targets here would silently strip a surviving
|
|
// agent's allow-list.
|
|
func (q *Queries) DeleteAgentInvocationTargetsBySystemRuntimeAgents(ctx context.Context, runtimeID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteAgentInvocationTargetsBySystemRuntimeAgents, runtimeID)
|
|
return err
|
|
}
|
|
|
|
const listAgentInvocationTargets = `-- name: ListAgentInvocationTargets :many
|
|
|
|
SELECT id, agent_id, target_type, target_id, created_by, created_at FROM agent_invocation_target
|
|
WHERE agent_id = $1
|
|
ORDER BY target_type ASC, created_at ASC
|
|
`
|
|
|
|
// Agent invocation permission targets (MUL-3963). Rows are the allow-list for
|
|
// agents whose permission_mode = 'public_to'. See migration 130.
|
|
func (q *Queries) ListAgentInvocationTargets(ctx context.Context, agentID pgtype.UUID) ([]AgentInvocationTarget, error) {
|
|
rows, err := q.db.Query(ctx, listAgentInvocationTargets, agentID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []AgentInvocationTarget{}
|
|
for rows.Next() {
|
|
var i AgentInvocationTarget
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.AgentID,
|
|
&i.TargetType,
|
|
&i.TargetID,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAgentInvocationTargetsByAgentIDs = `-- name: ListAgentInvocationTargetsByAgentIDs :many
|
|
SELECT id, agent_id, target_type, target_id, created_by, created_at FROM agent_invocation_target
|
|
WHERE agent_id = ANY($1::uuid[])
|
|
ORDER BY agent_id, target_type ASC, created_at ASC
|
|
`
|
|
|
|
// Batch load for the agent list endpoint so we don't N+1 per agent.
|
|
func (q *Queries) ListAgentInvocationTargetsByAgentIDs(ctx context.Context, agentIds []pgtype.UUID) ([]AgentInvocationTarget, error) {
|
|
rows, err := q.db.Query(ctx, listAgentInvocationTargetsByAgentIDs, agentIds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []AgentInvocationTarget{}
|
|
for rows.Next() {
|
|
var i AgentInvocationTarget
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.AgentID,
|
|
&i.TargetType,
|
|
&i.TargetID,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|