mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-05 09:30:05 +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>
456 lines
14 KiB
Go
456 lines
14 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: runtime_profile.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const countAgentsByProfile = `-- name: CountAgentsByProfile :one
|
|
SELECT count(*) FROM agent a
|
|
JOIN agent_runtime ar ON ar.id = a.runtime_id
|
|
WHERE ar.profile_id = $1 AND ar.workspace_id = $2 AND a.archived_at IS NULL
|
|
`
|
|
|
|
type CountAgentsByProfileParams struct {
|
|
ProfileID pgtype.UUID `json:"profile_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
// Counts active (non-archived) agents bound to any runtime instance of this
|
|
// profile. The profile-delete path uses this to refuse deletion (409) while
|
|
// agents still depend on it, mirroring the runtime-delete guard.
|
|
func (q *Queries) CountAgentsByProfile(ctx context.Context, arg CountAgentsByProfileParams) (int64, error) {
|
|
row := q.db.QueryRow(ctx, countAgentsByProfile, arg.ProfileID, arg.WorkspaceID)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const createRuntimeProfile = `-- name: CreateRuntimeProfile :one
|
|
|
|
INSERT INTO runtime_profile (
|
|
workspace_id,
|
|
display_name,
|
|
protocol_family,
|
|
command_name,
|
|
description,
|
|
fixed_args,
|
|
visibility,
|
|
created_by,
|
|
enabled
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
RETURNING id, workspace_id, display_name, protocol_family, command_name, description, fixed_args, visibility, created_by, enabled, created_at, updated_at
|
|
`
|
|
|
|
type CreateRuntimeProfileParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
DisplayName string `json:"display_name"`
|
|
ProtocolFamily string `json:"protocol_family"`
|
|
CommandName string `json:"command_name"`
|
|
Description pgtype.Text `json:"description"`
|
|
FixedArgs []byte `json:"fixed_args"`
|
|
Visibility string `json:"visibility"`
|
|
CreatedBy pgtype.UUID `json:"created_by"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
// Custom Runtime profiles (MUL-3284). Workspace-level definitions of a custom
|
|
// runtime; see migration 120 for the table. Relational integrity (workspace,
|
|
// created_by) is enforced in the application layer — there are no DB FKs.
|
|
func (q *Queries) CreateRuntimeProfile(ctx context.Context, arg CreateRuntimeProfileParams) (RuntimeProfile, error) {
|
|
row := q.db.QueryRow(ctx, createRuntimeProfile,
|
|
arg.WorkspaceID,
|
|
arg.DisplayName,
|
|
arg.ProtocolFamily,
|
|
arg.CommandName,
|
|
arg.Description,
|
|
arg.FixedArgs,
|
|
arg.Visibility,
|
|
arg.CreatedBy,
|
|
arg.Enabled,
|
|
)
|
|
var i RuntimeProfile
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.DisplayName,
|
|
&i.ProtocolFamily,
|
|
&i.CommandName,
|
|
&i.Description,
|
|
&i.FixedArgs,
|
|
&i.Visibility,
|
|
&i.CreatedBy,
|
|
&i.Enabled,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteAgentRuntimesByProfile = `-- name: DeleteAgentRuntimesByProfile :many
|
|
DELETE FROM agent_runtime
|
|
WHERE profile_id = $1 AND workspace_id = $2
|
|
RETURNING id, workspace_id, owner_id, daemon_id, provider
|
|
`
|
|
|
|
type DeleteAgentRuntimesByProfileParams struct {
|
|
ProfileID pgtype.UUID `json:"profile_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
type DeleteAgentRuntimesByProfileRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
OwnerID pgtype.UUID `json:"owner_id"`
|
|
DaemonID pgtype.Text `json:"daemon_id"`
|
|
Provider string `json:"provider"`
|
|
}
|
|
|
|
// Application-layer cascade: migration 120 dropped the DB ON DELETE CASCADE, so
|
|
// the profile-delete path must remove the profile's registered runtime
|
|
// instances itself. Returns the deleted rows so the caller can broadcast /
|
|
// audit. Runs inside the same transaction as DeleteRuntimeProfile.
|
|
func (q *Queries) DeleteAgentRuntimesByProfile(ctx context.Context, arg DeleteAgentRuntimesByProfileParams) ([]DeleteAgentRuntimesByProfileRow, error) {
|
|
rows, err := q.db.Query(ctx, deleteAgentRuntimesByProfile, arg.ProfileID, arg.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []DeleteAgentRuntimesByProfileRow{}
|
|
for rows.Next() {
|
|
var i DeleteAgentRuntimesByProfileRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.OwnerID,
|
|
&i.DaemonID,
|
|
&i.Provider,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const deleteRuntimeProfile = `-- name: DeleteRuntimeProfile :exec
|
|
DELETE FROM runtime_profile
|
|
WHERE id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type DeleteRuntimeProfileParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) DeleteRuntimeProfile(ctx context.Context, arg DeleteRuntimeProfileParams) error {
|
|
_, err := q.db.Exec(ctx, deleteRuntimeProfile, arg.ID, arg.WorkspaceID)
|
|
return err
|
|
}
|
|
|
|
const getRuntimeProfile = `-- name: GetRuntimeProfile :one
|
|
SELECT id, workspace_id, display_name, protocol_family, command_name, description, fixed_args, visibility, created_by, enabled, created_at, updated_at FROM runtime_profile
|
|
WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetRuntimeProfile(ctx context.Context, id pgtype.UUID) (RuntimeProfile, error) {
|
|
row := q.db.QueryRow(ctx, getRuntimeProfile, id)
|
|
var i RuntimeProfile
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.DisplayName,
|
|
&i.ProtocolFamily,
|
|
&i.CommandName,
|
|
&i.Description,
|
|
&i.FixedArgs,
|
|
&i.Visibility,
|
|
&i.CreatedBy,
|
|
&i.Enabled,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getRuntimeProfileForWorkspace = `-- name: GetRuntimeProfileForWorkspace :one
|
|
SELECT id, workspace_id, display_name, protocol_family, command_name, description, fixed_args, visibility, created_by, enabled, created_at, updated_at FROM runtime_profile
|
|
WHERE id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type GetRuntimeProfileForWorkspaceParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) GetRuntimeProfileForWorkspace(ctx context.Context, arg GetRuntimeProfileForWorkspaceParams) (RuntimeProfile, error) {
|
|
row := q.db.QueryRow(ctx, getRuntimeProfileForWorkspace, arg.ID, arg.WorkspaceID)
|
|
var i RuntimeProfile
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.DisplayName,
|
|
&i.ProtocolFamily,
|
|
&i.CommandName,
|
|
&i.Description,
|
|
&i.FixedArgs,
|
|
&i.Visibility,
|
|
&i.CreatedBy,
|
|
&i.Enabled,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listAgentRuntimeIDsByProfile = `-- name: ListAgentRuntimeIDsByProfile :many
|
|
SELECT id FROM agent_runtime
|
|
WHERE profile_id = $1 AND workspace_id = $2
|
|
ORDER BY id
|
|
FOR UPDATE
|
|
`
|
|
|
|
type ListAgentRuntimeIDsByProfileParams struct {
|
|
ProfileID pgtype.UUID `json:"profile_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
// Enumerates the runtime instance rows registered against a profile. The
|
|
// profile-delete cascade walks these so it can run the same archived-agent /
|
|
// archived-squad / autopilot teardown the runtime-delete path uses before
|
|
// removing each runtime row — agent.runtime_id is ON DELETE RESTRICT, so a
|
|
// bare delete would 500 whenever an archived agent still references the row.
|
|
func (q *Queries) ListAgentRuntimeIDsByProfile(ctx context.Context, arg ListAgentRuntimeIDsByProfileParams) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, listAgentRuntimeIDsByProfile, arg.ProfileID, arg.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []pgtype.UUID{}
|
|
for rows.Next() {
|
|
var id pgtype.UUID
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listEnabledRuntimeProfilesForWorkspace = `-- name: ListEnabledRuntimeProfilesForWorkspace :many
|
|
SELECT id, workspace_id, display_name, protocol_family, command_name, description, fixed_args, visibility, created_by, enabled, created_at, updated_at FROM runtime_profile
|
|
WHERE workspace_id = $1 AND enabled = true
|
|
ORDER BY created_at ASC
|
|
`
|
|
|
|
// Daemon-facing list: only enabled profiles are candidates for a daemon to
|
|
// resolve on PATH and register. Ordered for stable output.
|
|
func (q *Queries) ListEnabledRuntimeProfilesForWorkspace(ctx context.Context, workspaceID pgtype.UUID) ([]RuntimeProfile, error) {
|
|
rows, err := q.db.Query(ctx, listEnabledRuntimeProfilesForWorkspace, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []RuntimeProfile{}
|
|
for rows.Next() {
|
|
var i RuntimeProfile
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.DisplayName,
|
|
&i.ProtocolFamily,
|
|
&i.CommandName,
|
|
&i.Description,
|
|
&i.FixedArgs,
|
|
&i.Visibility,
|
|
&i.CreatedBy,
|
|
&i.Enabled,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listRuntimeProfiles = `-- name: ListRuntimeProfiles :many
|
|
SELECT id, workspace_id, display_name, protocol_family, command_name, description, fixed_args, visibility, created_by, enabled, created_at, updated_at FROM runtime_profile
|
|
WHERE workspace_id = $1
|
|
ORDER BY created_at ASC
|
|
`
|
|
|
|
func (q *Queries) ListRuntimeProfiles(ctx context.Context, workspaceID pgtype.UUID) ([]RuntimeProfile, error) {
|
|
rows, err := q.db.Query(ctx, listRuntimeProfiles, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []RuntimeProfile{}
|
|
for rows.Next() {
|
|
var i RuntimeProfile
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.DisplayName,
|
|
&i.ProtocolFamily,
|
|
&i.CommandName,
|
|
&i.Description,
|
|
&i.FixedArgs,
|
|
&i.Visibility,
|
|
&i.CreatedBy,
|
|
&i.Enabled,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const lockRuntimeProfileForDelete = `-- name: LockRuntimeProfileForDelete :one
|
|
SELECT id, workspace_id, display_name, protocol_family, command_name, description, fixed_args, visibility, created_by, enabled, created_at, updated_at FROM runtime_profile
|
|
WHERE id = $1 AND workspace_id = $2
|
|
FOR UPDATE
|
|
`
|
|
|
|
type LockRuntimeProfileForDeleteParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
// See LockRuntimeProfileForRegistration. The stronger lock prevents a daemon
|
|
// from registering another instance between the delete plan and commit.
|
|
func (q *Queries) LockRuntimeProfileForDelete(ctx context.Context, arg LockRuntimeProfileForDeleteParams) (RuntimeProfile, error) {
|
|
row := q.db.QueryRow(ctx, lockRuntimeProfileForDelete, arg.ID, arg.WorkspaceID)
|
|
var i RuntimeProfile
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.DisplayName,
|
|
&i.ProtocolFamily,
|
|
&i.CommandName,
|
|
&i.Description,
|
|
&i.FixedArgs,
|
|
&i.Visibility,
|
|
&i.CreatedBy,
|
|
&i.Enabled,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const lockRuntimeProfileForRegistration = `-- name: LockRuntimeProfileForRegistration :one
|
|
SELECT id, workspace_id, display_name, protocol_family, command_name, description, fixed_args, visibility, created_by, enabled, created_at, updated_at FROM runtime_profile
|
|
WHERE id = $1 AND workspace_id = $2
|
|
FOR KEY SHARE
|
|
`
|
|
|
|
type LockRuntimeProfileForRegistrationParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
// Serializes daemon registration with profile deletion. Registration holds a
|
|
// KEY SHARE lock until its runtime row is committed; profile deletion takes an
|
|
// UPDATE lock, then locks the profile's runtime rows. Whichever starts first
|
|
// wins, so deletion cannot miss a runtime inserted from a stale profile read.
|
|
func (q *Queries) LockRuntimeProfileForRegistration(ctx context.Context, arg LockRuntimeProfileForRegistrationParams) (RuntimeProfile, error) {
|
|
row := q.db.QueryRow(ctx, lockRuntimeProfileForRegistration, arg.ID, arg.WorkspaceID)
|
|
var i RuntimeProfile
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.DisplayName,
|
|
&i.ProtocolFamily,
|
|
&i.CommandName,
|
|
&i.Description,
|
|
&i.FixedArgs,
|
|
&i.Visibility,
|
|
&i.CreatedBy,
|
|
&i.Enabled,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateRuntimeProfile = `-- name: UpdateRuntimeProfile :one
|
|
UPDATE runtime_profile
|
|
SET display_name = COALESCE($1, display_name),
|
|
command_name = COALESCE($2, command_name),
|
|
description = COALESCE($3, description),
|
|
fixed_args = COALESCE($4, fixed_args),
|
|
visibility = COALESCE($5, visibility),
|
|
enabled = COALESCE($6, enabled),
|
|
updated_at = now()
|
|
WHERE id = $7 AND workspace_id = $8
|
|
RETURNING id, workspace_id, display_name, protocol_family, command_name, description, fixed_args, visibility, created_by, enabled, created_at, updated_at
|
|
`
|
|
|
|
type UpdateRuntimeProfileParams struct {
|
|
DisplayName pgtype.Text `json:"display_name"`
|
|
CommandName pgtype.Text `json:"command_name"`
|
|
Description pgtype.Text `json:"description"`
|
|
FixedArgs []byte `json:"fixed_args"`
|
|
Visibility pgtype.Text `json:"visibility"`
|
|
Enabled pgtype.Bool `json:"enabled"`
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
// Partial update via COALESCE: NULL args leave the column unchanged. The
|
|
// protocol_family is intentionally NOT updatable — changing the underlying
|
|
// backend of an existing profile would silently repoint every agent bound to
|
|
// it onto a different protocol; callers create a new profile instead.
|
|
func (q *Queries) UpdateRuntimeProfile(ctx context.Context, arg UpdateRuntimeProfileParams) (RuntimeProfile, error) {
|
|
row := q.db.QueryRow(ctx, updateRuntimeProfile,
|
|
arg.DisplayName,
|
|
arg.CommandName,
|
|
arg.Description,
|
|
arg.FixedArgs,
|
|
arg.Visibility,
|
|
arg.Enabled,
|
|
arg.ID,
|
|
arg.WorkspaceID,
|
|
)
|
|
var i RuntimeProfile
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.DisplayName,
|
|
&i.ProtocolFamily,
|
|
&i.CommandName,
|
|
&i.Description,
|
|
&i.FixedArgs,
|
|
&i.Visibility,
|
|
&i.CreatedBy,
|
|
&i.Enabled,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|