mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
Resolves the UpdateAgent conflict in handler/agent.go by keeping both the connector_service_allowlist write and main's runtime-binding transaction, renumbers the open-connector migrations to 590-596 because main took 583/584, and regenerates sqlc. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01XqqpKySn2iWtXriK3Gsgak
9259 lines
346 KiB
Go
9259 lines
346 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
||
// versions:
|
||
// sqlc v1.31.1
|
||
// source: agent.sql
|
||
|
||
package db
|
||
|
||
import (
|
||
"context"
|
||
|
||
"github.com/jackc/pgx/v5/pgtype"
|
||
)
|
||
|
||
const acknowledgeExhaustedDelegatedFailureRecovery = `-- name: AcknowledgeExhaustedDelegatedFailureRecovery :one
|
||
UPDATE agent_task_queue AS acknowledged
|
||
SET delivered_comment_ids = (
|
||
SELECT COALESCE(array_agg(DISTINCT receipt.id), '{}')::uuid[]
|
||
FROM unnest(array_append(acknowledged.delivered_comment_ids, $1::uuid)) AS receipt(id)
|
||
)
|
||
WHERE acknowledged.id = (
|
||
SELECT attempt.id
|
||
FROM agent_task_queue attempt
|
||
WHERE attempt.trigger_evidence_kind = 'delegated_failure'
|
||
AND attempt.trigger_evidence_ref_id = $2
|
||
ORDER BY attempt.created_at DESC, attempt.id DESC
|
||
LIMIT 1
|
||
FOR UPDATE
|
||
)
|
||
AND (
|
||
SELECT count(*)
|
||
FROM agent_task_queue attempt_count
|
||
WHERE attempt_count.trigger_evidence_kind = 'delegated_failure'
|
||
AND attempt_count.trigger_evidence_ref_id = $2
|
||
) >= $3::int
|
||
RETURNING acknowledged.id, acknowledged.agent_id, acknowledged.issue_id, acknowledged.status, acknowledged.priority, acknowledged.dispatched_at, acknowledged.started_at, acknowledged.completed_at, acknowledged.result, acknowledged.error, acknowledged.created_at, acknowledged.context, acknowledged.runtime_id, acknowledged.session_id, acknowledged.work_dir, acknowledged.trigger_comment_id, acknowledged.chat_session_id, acknowledged.autopilot_run_id, acknowledged.attempt, acknowledged.max_attempts, acknowledged.parent_task_id, acknowledged.failure_reason, acknowledged.trigger_summary, acknowledged.force_fresh_session, acknowledged.is_leader_task, acknowledged.wait_reason, acknowledged.initiator_user_id, acknowledged.handoff_note, acknowledged.prepare_lease_expires_at, acknowledged.squad_id, acknowledged.runtime_mcp_overlay, acknowledged.escalation_for_task_id, acknowledged.fire_at, acknowledged.originator_user_id, acknowledged.runtime_connected_apps, acknowledged.coalesced_comment_ids, acknowledged.delivered_comment_ids, acknowledged.chat_input_task_id, acknowledged.chat_finalize_deferred_at, acknowledged.originator_source, acknowledged.delegated_from_task_id, acknowledged.retry_of_task_id, acknowledged.rerun_of_task_id, acknowledged.rule_version_id, acknowledged.trigger_evidence_kind, acknowledged.trigger_evidence_ref_id, acknowledged.accountable_user_id, acknowledged.session_rollout_missing, acknowledged.retired_session_id, acknowledged.quick_actions_disabled, acknowledged.regenerate_quick_actions_for, acknowledged.branch_name, acknowledged.durable_work_dir, acknowledged.channel_context_revision, acknowledged.comment_thread_id, acknowledged.cancelled_by_type, acknowledged.cancelled_by_id, acknowledged.cancelled_by_name, acknowledged.issue_snapshot, acknowledged.workflow_step, acknowledged.runtime_connector_handles
|
||
`
|
||
|
||
type AcknowledgeExhaustedDelegatedFailureRecoveryParams struct {
|
||
CommentID pgtype.UUID `json:"comment_id"`
|
||
FailedTaskID pgtype.UUID `json:"failed_task_id"`
|
||
MaxAttempts int32 `json:"max_attempts"`
|
||
}
|
||
|
||
// Once the bounded automatic attempts are exhausted, record a terminal
|
||
// acknowledgement on the newest recovery task. The outbox treats this receipt
|
||
// as settled, while the service writes a separate visible system comment that
|
||
// tells the user why no further task will be generated.
|
||
func (q *Queries) AcknowledgeExhaustedDelegatedFailureRecovery(ctx context.Context, arg AcknowledgeExhaustedDelegatedFailureRecoveryParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, acknowledgeExhaustedDelegatedFailureRecovery, arg.CommentID, arg.FailedTaskID, arg.MaxAttempts)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const archiveAgent = `-- name: ArchiveAgent :one
|
||
UPDATE agent SET archived_at = now(), archived_by = $2, updated_at = now()
|
||
WHERE id = $1
|
||
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist
|
||
`
|
||
|
||
type ArchiveAgentParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
ArchivedBy pgtype.UUID `json:"archived_by"`
|
||
}
|
||
|
||
func (q *Queries) ArchiveAgent(ctx context.Context, arg ArchiveAgentParams) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, archiveAgent, arg.ID, arg.ArchivedBy)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const archiveAgentsByIDs = `-- name: ArchiveAgentsByIDs :many
|
||
UPDATE agent
|
||
SET archived_at = now(), archived_by = $1, updated_at = now()
|
||
WHERE id = ANY($2::uuid[]) AND archived_at IS NULL
|
||
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist
|
||
`
|
||
|
||
type ArchiveAgentsByIDsParams struct {
|
||
ArchivedBy pgtype.UUID `json:"archived_by"`
|
||
AgentIds []pgtype.UUID `json:"agent_ids"`
|
||
}
|
||
|
||
// Narrow archive that only touches the explicit ID list. Used by the
|
||
// cascade-delete endpoint so the user's expected_active_agent_ids list
|
||
// is the authoritative bound on what gets archived: any agent that
|
||
// appeared on the runtime after the user opened the dialog is filtered
|
||
// out here so it can't be silently archived even in the (vanishingly
|
||
// rare) case where a row-level race slips past the runtime FOR UPDATE
|
||
// lock. Returns the affected rows so the caller can broadcast
|
||
// agent:archived per agent.
|
||
func (q *Queries) ArchiveAgentsByIDs(ctx context.Context, arg ArchiveAgentsByIDsParams) ([]Agent, error) {
|
||
rows, err := q.db.Query(ctx, archiveAgentsByIDs, arg.ArchivedBy, arg.AgentIds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []Agent{}
|
||
for rows.Next() {
|
||
var i Agent
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const archiveAgentsByRuntime = `-- name: ArchiveAgentsByRuntime :many
|
||
UPDATE agent
|
||
SET archived_at = now(), archived_by = $1, updated_at = now()
|
||
WHERE runtime_id = ANY($2::uuid[]) AND archived_at IS NULL
|
||
AND (system_key IS NULL OR system_key = '')
|
||
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist
|
||
`
|
||
|
||
type ArchiveAgentsByRuntimeParams struct {
|
||
ArchivedBy pgtype.UUID `json:"archived_by"`
|
||
RuntimeIds []pgtype.UUID `json:"runtime_ids"`
|
||
}
|
||
|
||
// Bulk-archives every active agent bound to any runtime in the given set.
|
||
// Used when revoking a leaving member's runtimes so agents pinned to those
|
||
// runtimes can no longer be assigned new work. Returns the affected rows so
|
||
// the caller can broadcast agent:archived per agent.
|
||
//
|
||
// System agents are exempt: they belong to the workspace rather than to the
|
||
// member who happened to create them, and the workspace's entry point runs
|
||
// through one. Archiving Mika because a colleague left would take the default
|
||
// agent away from everyone. Its runtime does go offline with the departure, so
|
||
// it needs rebinding — but it stays visible and recoverable instead of
|
||
// vanishing.
|
||
func (q *Queries) ArchiveAgentsByRuntime(ctx context.Context, arg ArchiveAgentsByRuntimeParams) ([]Agent, error) {
|
||
rows, err := q.db.Query(ctx, archiveAgentsByRuntime, arg.ArchivedBy, arg.RuntimeIds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []Agent{}
|
||
for rows.Next() {
|
||
var i Agent
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const cancelAgentTask = `-- name: CancelAgentTask :one
|
||
UPDATE agent_task_queue
|
||
SET status = 'cancelled', completed_at = now(), prepare_lease_expires_at = NULL,
|
||
cancelled_by_type = 'system', cancelled_by_id = NULL, cancelled_by_name = NULL
|
||
WHERE id = $1 AND status IN ('queued', 'dispatched', 'running', 'waiting_local_directory', 'deferred')
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
// Automatic cancellation without an explicit persisted failure reason. Unlike
|
||
// CancelAgentTaskByUser, this deliberately leaves recovery inputs replayable.
|
||
func (q *Queries) CancelAgentTask(ctx context.Context, id pgtype.UUID) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, cancelAgentTask, id)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const cancelAgentTaskByUser = `-- name: CancelAgentTaskByUser :one
|
||
UPDATE agent_task_queue AS task
|
||
SET status = 'cancelled',
|
||
completed_at = now(),
|
||
prepare_lease_expires_at = NULL,
|
||
cancelled_by_type = $2,
|
||
cancelled_by_id = $3,
|
||
cancelled_by_name = $4,
|
||
delivered_comment_ids = CASE
|
||
-- Chat and ordinary issue tasks almost never carry a delegated-failure
|
||
-- recovery signal. Keep their high-frequency user-cancel path to a
|
||
-- no-join update; only validate task lineage after the cheap comment
|
||
-- shape probe finds a possible recovery signal.
|
||
WHEN task.trigger_comment_id IS NULL
|
||
AND COALESCE(cardinality(task.coalesced_comment_ids), 0) = 0
|
||
THEN task.delivered_comment_ids
|
||
WHEN NOT EXISTS (
|
||
SELECT 1
|
||
FROM comment recovery_signal
|
||
WHERE (
|
||
recovery_signal.id = task.trigger_comment_id
|
||
OR recovery_signal.id = ANY(task.coalesced_comment_ids)
|
||
)
|
||
AND recovery_signal.author_type = 'system'
|
||
AND recovery_signal.type = 'progress_update'
|
||
AND recovery_signal.source_task_id IS NOT NULL
|
||
) THEN task.delivered_comment_ids
|
||
ELSE (
|
||
SELECT COALESCE(array_agg(DISTINCT receipt.id), '{}')::uuid[]
|
||
FROM unnest(array_cat(
|
||
task.delivered_comment_ids,
|
||
ARRAY(
|
||
SELECT recovery.id
|
||
FROM comment recovery
|
||
JOIN agent_task_queue failed ON failed.id = recovery.source_task_id
|
||
JOIN agent_task_queue source ON source.id = failed.delegated_from_task_id
|
||
WHERE (
|
||
recovery.id = task.trigger_comment_id
|
||
OR recovery.id = ANY(task.coalesced_comment_ids)
|
||
)
|
||
AND recovery.author_type = 'system'
|
||
AND recovery.type = 'progress_update'
|
||
AND recovery.source_task_id IS NOT NULL
|
||
AND failed.status = 'failed'
|
||
AND failed.delegated_from_task_id IS NOT NULL
|
||
AND failed.autopilot_run_id IS NULL
|
||
AND failed.trigger_evidence_kind IS DISTINCT FROM 'delegated_failure'
|
||
AND source.autopilot_run_id IS NULL
|
||
AND source.issue_id = task.issue_id
|
||
AND source.agent_id = task.agent_id
|
||
AND recovery.issue_id = source.issue_id
|
||
)
|
||
)) AS receipt(id)
|
||
)
|
||
END
|
||
WHERE task.id = $1
|
||
AND task.status IN ('queued', 'dispatched', 'running', 'waiting_local_directory', 'deferred')
|
||
RETURNING task.id, task.agent_id, task.issue_id, task.status, task.priority, task.dispatched_at, task.started_at, task.completed_at, task.result, task.error, task.created_at, task.context, task.runtime_id, task.session_id, task.work_dir, task.trigger_comment_id, task.chat_session_id, task.autopilot_run_id, task.attempt, task.max_attempts, task.parent_task_id, task.failure_reason, task.trigger_summary, task.force_fresh_session, task.is_leader_task, task.wait_reason, task.initiator_user_id, task.handoff_note, task.prepare_lease_expires_at, task.squad_id, task.runtime_mcp_overlay, task.escalation_for_task_id, task.fire_at, task.originator_user_id, task.runtime_connected_apps, task.coalesced_comment_ids, task.delivered_comment_ids, task.chat_input_task_id, task.chat_finalize_deferred_at, task.originator_source, task.delegated_from_task_id, task.retry_of_task_id, task.rerun_of_task_id, task.rule_version_id, task.trigger_evidence_kind, task.trigger_evidence_ref_id, task.accountable_user_id, task.session_rollout_missing, task.retired_session_id, task.quick_actions_disabled, task.regenerate_quick_actions_for, task.branch_name, task.durable_work_dir, task.channel_context_revision, task.comment_thread_id, task.cancelled_by_type, task.cancelled_by_id, task.cancelled_by_name, task.issue_snapshot, task.workflow_step, task.runtime_connector_handles
|
||
`
|
||
|
||
type CancelAgentTaskByUserParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
CancelledByType pgtype.Text `json:"cancelled_by_type"`
|
||
CancelledByID pgtype.UUID `json:"cancelled_by_id"`
|
||
CancelledByName pgtype.Text `json:"cancelled_by_name"`
|
||
}
|
||
|
||
// An explicit user cancellation is a terminal acknowledgement for any
|
||
// delegated-failure recovery signal planned into this task. Server-initiated
|
||
// cancellations keep using CancelAgentTask / CancelAgentTaskWithReason so a
|
||
// stale plan, claim failure, or other automatic repair can still be replayed.
|
||
func (q *Queries) CancelAgentTaskByUser(ctx context.Context, arg CancelAgentTaskByUserParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, cancelAgentTaskByUser,
|
||
arg.ID,
|
||
arg.CancelledByType,
|
||
arg.CancelledByID,
|
||
arg.CancelledByName,
|
||
)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const cancelAgentTaskWithReason = `-- name: CancelAgentTaskWithReason :one
|
||
UPDATE agent_task_queue
|
||
SET status = 'cancelled',
|
||
completed_at = now(),
|
||
error = $1,
|
||
failure_reason = $2,
|
||
prepare_lease_expires_at = NULL,
|
||
cancelled_by_type = 'system',
|
||
cancelled_by_id = NULL,
|
||
cancelled_by_name = NULL
|
||
WHERE id = $3 AND status IN ('queued', 'dispatched', 'running', 'waiting_local_directory', 'deferred')
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type CancelAgentTaskWithReasonParams struct {
|
||
Error pgtype.Text `json:"error"`
|
||
FailureReason pgtype.Text `json:"failure_reason"`
|
||
ID pgtype.UUID `json:"id"`
|
||
}
|
||
|
||
// Cancels a task AND records why, for cancellations the user did not ask for.
|
||
//
|
||
// CancelAgentTaskByUser leaves error/failure_reason NULL because the user knows
|
||
// why. A server-initiated refusal is the opposite: without a persisted reason
|
||
// the run surfaces as an unexplained "cancelled", and the only trace is a 4xx
|
||
// in a daemon log the user never sees. Retrying cannot help either (the task is
|
||
// refused for a durable reason), so this terminal state carries its explanation.
|
||
func (q *Queries) CancelAgentTaskWithReason(ctx context.Context, arg CancelAgentTaskWithReasonParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, cancelAgentTaskWithReason, arg.Error, arg.FailureReason, arg.ID)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const cancelAgentTasksByAgent = `-- name: CancelAgentTasksByAgent :many
|
||
UPDATE agent_task_queue
|
||
SET status = 'cancelled', completed_at = now(), prepare_lease_expires_at = NULL,
|
||
cancelled_by_type = 'system', cancelled_by_id = NULL, cancelled_by_name = NULL
|
||
WHERE agent_id = $1 AND status IN ('queued', 'dispatched', 'running', 'waiting_local_directory', 'deferred')
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
// Bulk-cancel every active (queued/dispatched/running) task for an agent.
|
||
// Returns the affected rows so callers can broadcast task:cancelled events.
|
||
// Mirrors the shape of CancelAgentTasksByIssue / CancelPendingTasksByIssueAndAgent
|
||
// (also :many + RETURNING + completed_at) so the three sibling cancel paths
|
||
// behave consistently.
|
||
func (q *Queries) CancelAgentTasksByAgent(ctx context.Context, agentID pgtype.UUID) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, cancelAgentTasksByAgent, agentID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const cancelAgentTasksByChatSession = `-- name: CancelAgentTasksByChatSession :many
|
||
UPDATE agent_task_queue
|
||
SET status = 'cancelled', completed_at = now(), prepare_lease_expires_at = NULL,
|
||
cancelled_by_type = 'system', cancelled_by_id = NULL, cancelled_by_name = NULL
|
||
WHERE chat_session_id = $1 AND status IN ('queued', 'dispatched', 'running', 'waiting_local_directory', 'deferred')
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
// Cancels active tasks belonging to a chat session. Called from
|
||
// DeleteChatSession so the daemon doesn't keep running work whose result
|
||
// has nowhere to land. Must run BEFORE the chat_session row is deleted —
|
||
// the FK ON DELETE SET NULL would otherwise nullify chat_session_id and we
|
||
// could no longer reach those tasks.
|
||
func (q *Queries) CancelAgentTasksByChatSession(ctx context.Context, chatSessionID pgtype.UUID) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, cancelAgentTasksByChatSession, chatSessionID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const cancelAgentTasksByIssue = `-- name: CancelAgentTasksByIssue :many
|
||
UPDATE agent_task_queue
|
||
SET status = 'cancelled', completed_at = now(), prepare_lease_expires_at = NULL,
|
||
cancelled_by_type = 'system', cancelled_by_id = NULL, cancelled_by_name = NULL
|
||
WHERE issue_id = $1 AND status IN ('queued', 'dispatched', 'running', 'waiting_local_directory', 'deferred')
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
// Cancels every active task on the issue and returns the affected rows so the
|
||
// caller can reconcile each agent's status and broadcast task:cancelled events
|
||
// (#1587). Prior :exec form silently dropped that info, leaving agents stuck at
|
||
// status="working" with no self-correction. Only issue-deletion cleanup calls
|
||
// this now; a status flip to cancelled/done no longer does (MUL-4465).
|
||
func (q *Queries) CancelAgentTasksByIssue(ctx context.Context, issueID pgtype.UUID) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, cancelAgentTasksByIssue, issueID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const cancelAgentTasksByTriggerComment = `-- name: CancelAgentTasksByTriggerComment :many
|
||
UPDATE agent_task_queue
|
||
SET status = 'cancelled', completed_at = now(), prepare_lease_expires_at = NULL,
|
||
cancelled_by_type = 'system', cancelled_by_id = NULL, cancelled_by_name = NULL,
|
||
context = COALESCE(context, '{}'::jsonb) || jsonb_build_object('comment_change_cancelled_task_id', id::text)
|
||
WHERE (trigger_comment_id = $1 OR $1 = ANY(coalesced_comment_ids))
|
||
AND status IN ('queued', 'dispatched', 'running', 'waiting_local_directory', 'deferred')
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
// Cancels active tasks whose planned batch contains the edited/deleted comment.
|
||
// The body may already have been embedded as either the primary trigger or a
|
||
// coalesced input; cancellation prevents an agent from acting on a stale or
|
||
// deleted version. Must run before deletion clears trigger_comment_id.
|
||
func (q *Queries) CancelAgentTasksByTriggerComment(ctx context.Context, triggerCommentID pgtype.UUID) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, cancelAgentTasksByTriggerComment, triggerCommentID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const cancelPendingTasksByIssueAndAgent = `-- name: CancelPendingTasksByIssueAndAgent :many
|
||
UPDATE agent_task_queue
|
||
SET status = 'cancelled', completed_at = now(), prepare_lease_expires_at = NULL,
|
||
cancelled_by_type = 'system', cancelled_by_id = NULL, cancelled_by_name = NULL
|
||
WHERE issue_id = $1 AND agent_id = $2
|
||
AND status IN ('queued', 'dispatched', 'deferred')
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type CancelPendingTasksByIssueAndAgentParams struct {
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
}
|
||
|
||
// Cancels the not-yet-started tasks for a single (issue, agent) pair, so the
|
||
// manual rerun flow can enqueue a replacement without colliding with
|
||
// idx_one_pending_task_per_issue_agent_v2 and without dropping the rerun's own
|
||
// attribution onto a row somebody else created.
|
||
//
|
||
// 'running' and 'waiting_local_directory' are deliberately NOT cancelled: an
|
||
// agent is executing in them. Neither status appears in that unique index, so a
|
||
// fresh queued row can be inserted alongside one, and ClaimAgentTask's
|
||
// per-(issue, agent) serialization holds the new row until the active run
|
||
// reaches a terminal state. Cancelling them made a manual rerun kill the pass
|
||
// the agent was still working on; interrupting an in-flight run is CancelTask's
|
||
// job, not rerun's.
|
||
//
|
||
// Everything that has NOT begun executing is still cleared, including deferred
|
||
// escalations, so rerun keeps its prior "replace the pending plan" behaviour for
|
||
// those rows.
|
||
func (q *Queries) CancelPendingTasksByIssueAndAgent(ctx context.Context, arg CancelPendingTasksByIssueAndAgentParams) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, cancelPendingTasksByIssueAndAgent, arg.IssueID, arg.AgentID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const cancelPendingTasksByIssueAndAgentInThread = `-- name: CancelPendingTasksByIssueAndAgentInThread :many
|
||
UPDATE agent_task_queue
|
||
SET status = 'cancelled', completed_at = now(), prepare_lease_expires_at = NULL,
|
||
cancelled_by_type = 'system', cancelled_by_id = NULL, cancelled_by_name = NULL
|
||
WHERE issue_id = $1 AND agent_id = $2
|
||
AND status IN ('queued', 'dispatched', 'deferred')
|
||
AND comment_thread_id IS NOT DISTINCT FROM comment_thread_root_id($3::uuid)
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type CancelPendingTasksByIssueAndAgentInThreadParams struct {
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
ThreadCommentID pgtype.UUID `json:"thread_comment_id"`
|
||
}
|
||
|
||
// Cancel only the not-yet-started plan in the selected thread. Other threads
|
||
// retain their queues; running tasks are stopped explicitly through CancelTask.
|
||
func (q *Queries) CancelPendingTasksByIssueAndAgentInThread(ctx context.Context, arg CancelPendingTasksByIssueAndAgentInThreadParams) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, cancelPendingTasksByIssueAndAgentInThread, arg.IssueID, arg.AgentID, arg.ThreadCommentID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const cancelQueuedAgentTask = `-- name: CancelQueuedAgentTask :one
|
||
UPDATE agent_task_queue
|
||
SET status = 'cancelled', completed_at = now(), prepare_lease_expires_at = NULL,
|
||
cancelled_by_type = $1,
|
||
cancelled_by_id = $2,
|
||
cancelled_by_name = $3
|
||
WHERE id = $4
|
||
AND chat_session_id = $5
|
||
AND status = 'queued'
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type CancelQueuedAgentTaskParams struct {
|
||
CancelledByType pgtype.Text `json:"cancelled_by_type"`
|
||
CancelledByID pgtype.UUID `json:"cancelled_by_id"`
|
||
CancelledByName pgtype.Text `json:"cancelled_by_name"`
|
||
ID pgtype.UUID `json:"id"`
|
||
ChatSessionID pgtype.UUID `json:"chat_session_id"`
|
||
}
|
||
|
||
// Queue editing is a compare-and-set: never cancel a task that the daemon
|
||
// promoted between the user's click and this statement.
|
||
func (q *Queries) CancelQueuedAgentTask(ctx context.Context, arg CancelQueuedAgentTaskParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, cancelQueuedAgentTask,
|
||
arg.CancelledByType,
|
||
arg.CancelledByID,
|
||
arg.CancelledByName,
|
||
arg.ID,
|
||
arg.ChatSessionID,
|
||
)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const cancelQueuedAgentTasksForSession = `-- name: CancelQueuedAgentTasksForSession :many
|
||
WITH head AS MATERIALIZED (
|
||
SELECT candidate.id
|
||
FROM agent_task_queue AS candidate
|
||
WHERE candidate.chat_session_id = $1
|
||
AND candidate.status IN ('queued', 'dispatched', 'running', 'waiting_local_directory', 'deferred')
|
||
AND candidate.regenerate_quick_actions_for IS NULL
|
||
ORDER BY
|
||
CASE
|
||
WHEN candidate.status IN ('dispatched', 'running', 'waiting_local_directory') THEN 0
|
||
WHEN candidate.status = 'deferred' THEN 1
|
||
ELSE 2
|
||
END,
|
||
candidate.priority DESC,
|
||
candidate.created_at ASC,
|
||
candidate.id ASC
|
||
LIMIT 1
|
||
)
|
||
UPDATE agent_task_queue AS queued
|
||
SET status = 'cancelled', completed_at = now(), prepare_lease_expires_at = NULL,
|
||
cancelled_by_type = 'system', cancelled_by_id = NULL, cancelled_by_name = NULL
|
||
WHERE queued.chat_session_id = $1
|
||
AND queued.status = 'queued'
|
||
AND queued.id IS DISTINCT FROM (SELECT id FROM head)
|
||
RETURNING queued.id, queued.agent_id, queued.issue_id, queued.status, queued.priority, queued.dispatched_at, queued.started_at, queued.completed_at, queued.result, queued.error, queued.created_at, queued.context, queued.runtime_id, queued.session_id, queued.work_dir, queued.trigger_comment_id, queued.chat_session_id, queued.autopilot_run_id, queued.attempt, queued.max_attempts, queued.parent_task_id, queued.failure_reason, queued.trigger_summary, queued.force_fresh_session, queued.is_leader_task, queued.wait_reason, queued.initiator_user_id, queued.handoff_note, queued.prepare_lease_expires_at, queued.squad_id, queued.runtime_mcp_overlay, queued.escalation_for_task_id, queued.fire_at, queued.originator_user_id, queued.runtime_connected_apps, queued.coalesced_comment_ids, queued.delivered_comment_ids, queued.chat_input_task_id, queued.chat_finalize_deferred_at, queued.originator_source, queued.delegated_from_task_id, queued.retry_of_task_id, queued.rerun_of_task_id, queued.rule_version_id, queued.trigger_evidence_kind, queued.trigger_evidence_ref_id, queued.accountable_user_id, queued.session_rollout_missing, queued.retired_session_id, queued.quick_actions_disabled, queued.regenerate_quick_actions_for, queued.branch_name, queued.durable_work_dir, queued.channel_context_revision, queued.comment_thread_id, queued.cancelled_by_type, queued.cancelled_by_id, queued.cancelled_by_name, queued.issue_snapshot, queued.workflow_step, queued.runtime_connector_handles
|
||
`
|
||
|
||
// Clear only positional follow-ups. The first visible task is current even
|
||
// when it has not been claimed yet, so an all-queued session must preserve its
|
||
// priority/FIFO head. Keep this selector in lockstep with the visible-head
|
||
// invariant documented above ListChatMessages in chat.sql.
|
||
func (q *Queries) CancelQueuedAgentTasksForSession(ctx context.Context, chatSessionID pgtype.UUID) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, cancelQueuedAgentTasksForSession, chatSessionID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const cancelSupersededDeferredRetriesForRuntimes = `-- name: CancelSupersededDeferredRetriesForRuntimes :many
|
||
UPDATE agent_task_queue r
|
||
SET status = 'cancelled', completed_at = now(), prepare_lease_expires_at = NULL,
|
||
cancelled_by_type = 'system', cancelled_by_id = NULL, cancelled_by_name = NULL
|
||
WHERE r.runtime_id = ANY($1::uuid[])
|
||
AND r.status = 'deferred'
|
||
AND r.issue_id IS NOT NULL
|
||
AND r.retry_of_task_id IS NOT NULL
|
||
AND COALESCE(r.context->>'channel_issue_media_pending', '') <> 'true'
|
||
AND EXISTS (
|
||
SELECT 1 FROM agent_task_queue successor
|
||
WHERE successor.issue_id = r.issue_id
|
||
AND successor.agent_id = r.agent_id
|
||
AND successor.comment_thread_id IS NOT DISTINCT FROM r.comment_thread_id
|
||
AND successor.id <> r.id
|
||
AND successor.status IN ('queued', 'dispatched', 'running', 'waiting_local_directory')
|
||
)
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
// Cancels deferred auto-retry rows that a newer active task has already
|
||
// superseded, so one rerun click still means exactly one more run.
|
||
//
|
||
// The narrow race: a manual rerun clears the pending slot, a concurrent FailTask
|
||
// commits a deferred retry (runtime_offline / provider_network's final attempt
|
||
// arm fire_at), and the rerun's own enqueue then succeeds because 'deferred' is
|
||
// outside idx_one_pending_task_per_issue_agent_v2. Both rows now exist. Merely
|
||
// refusing to promote the retry is not enough: once the rerun stops occupying the
|
||
// slot the retry promotes and runs a SECOND time, duplicating the agent's
|
||
// comments, side effects and cost with nothing to signal it. That contradicts
|
||
// both "a manual rerun replaces the pending plan" and FailTask's own "a runnable
|
||
// successor already exists, so no retry is needed".
|
||
//
|
||
// Scope is deliberately tight:
|
||
// - retry_of_task_id IS NOT NULL — only auto-retry clones, never a fresh run.
|
||
// - channel-media pending rows are excluded for the same reason: that deferred
|
||
// row is the issue's own task waiting on media, not a superseded retry.
|
||
// - issue_id IS NOT NULL — chat / quick-create tasks have no slot semantics.
|
||
//
|
||
// 'running' and 'waiting_local_directory' count as superseding: the duplicate
|
||
// fires precisely when the rerun has STARTED, which is when the row would
|
||
// otherwise no longer look blocked.
|
||
func (q *Queries) CancelSupersededDeferredRetriesForRuntimes(ctx context.Context, runtimeIds []pgtype.UUID) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, cancelSupersededDeferredRetriesForRuntimes, runtimeIds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const claimAgentTask = `-- name: ClaimAgentTask :one
|
||
UPDATE agent_task_queue
|
||
SET status = 'dispatched',
|
||
dispatched_at = now(),
|
||
prepare_lease_expires_at = now() + make_interval(secs => $1::double precision)
|
||
WHERE id = (
|
||
SELECT atq.id FROM agent_task_queue atq
|
||
WHERE atq.agent_id = $2
|
||
AND atq.runtime_id = $3
|
||
AND atq.status = 'queued'
|
||
AND (atq.context->>'wakeup_id' IS NULL OR EXISTS (SELECT 1 FROM issue_wakeup w WHERE w.id=(atq.context->>'wakeup_id')::uuid AND w.disabled_at IS NULL AND w.revision=(atq.context->>'wakeup_revision')::bigint))
|
||
AND EXISTS (
|
||
SELECT 1
|
||
FROM agent a
|
||
JOIN agent_runtime r ON r.id = atq.runtime_id
|
||
WHERE a.id = atq.agent_id
|
||
-- A task's persisted runtime is not authority after an agent rebind.
|
||
AND a.runtime_id = atq.runtime_id
|
||
-- Queued private-runtime rows are claimable so the handler can
|
||
-- settle an owner mismatch through the existing FailTask path
|
||
-- before daemon delivery. Public runtimes remain shareable across
|
||
-- agent owners; dispatched reclaim keeps its owner fence below.
|
||
AND (
|
||
r.visibility = 'public'
|
||
OR r.visibility = 'private'
|
||
)
|
||
AND r.status = 'online'
|
||
AND COALESCE(r.last_seen_at, r.updated_at) >=
|
||
now() - make_interval(secs => $4::double precision)
|
||
)
|
||
AND NOT EXISTS (
|
||
SELECT 1 FROM agent_task_queue active
|
||
WHERE active.agent_id = atq.agent_id
|
||
AND active.status IN ('dispatched', 'running', 'waiting_local_directory')
|
||
AND (
|
||
(atq.issue_id IS NOT NULL AND active.issue_id = atq.issue_id)
|
||
OR (atq.chat_session_id IS NOT NULL AND active.chat_session_id = atq.chat_session_id)
|
||
OR (
|
||
atq.issue_id IS NULL
|
||
AND atq.chat_session_id IS NULL
|
||
AND atq.autopilot_run_id IS NULL
|
||
AND active.issue_id IS NULL
|
||
AND active.chat_session_id IS NULL
|
||
AND active.autopilot_run_id IS NULL
|
||
)
|
||
)
|
||
)
|
||
ORDER BY atq.priority DESC, atq.created_at ASC, atq.id ASC
|
||
LIMIT 1
|
||
FOR UPDATE SKIP LOCKED
|
||
)
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type ClaimAgentTaskParams struct {
|
||
PrepareLeaseSecs float64 `json:"prepare_lease_secs"`
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
RuntimeStaleSecs float64 `json:"runtime_stale_secs"`
|
||
}
|
||
|
||
// Claims the next queued task for an agent on one healthy runtime, enforcing
|
||
// per-(issue, agent) serialization:
|
||
// a task is only claimable when no other task for the same issue AND same agent is
|
||
// already dispatched or running. This allows different agents to work on the same
|
||
// issue in parallel while preventing a single agent from running duplicate tasks.
|
||
// Chat tasks (issue_id IS NULL) use chat_session_id for serialization instead.
|
||
// Quick-create tasks have no issue / chat / autopilot link, so they serialize on
|
||
// "any other quick-create-shaped task" (all four FKs NULL) for the same agent —
|
||
// otherwise a user mashing the create button could fire concurrent quick-creates
|
||
// whose completion lookup would race over "most recent issue by this agent".
|
||
func (q *Queries) ClaimAgentTask(ctx context.Context, arg ClaimAgentTaskParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, claimAgentTask,
|
||
arg.PrepareLeaseSecs,
|
||
arg.AgentID,
|
||
arg.RuntimeID,
|
||
arg.RuntimeStaleSecs,
|
||
)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const claimChatFinalizeDeferred = `-- name: ClaimChatFinalizeDeferred :one
|
||
UPDATE agent_task_queue
|
||
SET chat_finalize_deferred_at = NULL
|
||
WHERE id = $1 AND chat_finalize_deferred_at IS NOT NULL
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
// Atomically claims the deferred marker so the daemon ack and the sweeper
|
||
// cannot both finalize the same task (double-"Stopped." guard).
|
||
func (q *Queries) ClaimChatFinalizeDeferred(ctx context.Context, id pgtype.UUID) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, claimChatFinalizeDeferred, id)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const clearAgentComposioToolkitAllowlist = `-- name: ClearAgentComposioToolkitAllowlist :one
|
||
UPDATE agent SET composio_toolkit_allowlist = NULL, updated_at = now()
|
||
WHERE id = $1
|
||
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist
|
||
`
|
||
|
||
// Explicit NULL-clear for composio_toolkit_allowlist. The COALESCE-based
|
||
// UpdateAgent cannot set the column back to NULL — sending an empty array
|
||
// through there would persist `{}` (still a non-NULL, equivalent to "no
|
||
// toolkits" but distinct from "field never configured"). The API uses this
|
||
// dedicated query when the agent owner removes every toolkit; subsequent
|
||
// dispatch decisions treat NULL identically to `{}` (both -> no overlay).
|
||
func (q *Queries) ClearAgentComposioToolkitAllowlist(ctx context.Context, id pgtype.UUID) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, clearAgentComposioToolkitAllowlist, id)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const clearAgentConnectorServiceAllowlist = `-- name: ClearAgentConnectorServiceAllowlist :one
|
||
UPDATE agent SET connector_service_allowlist = NULL, updated_at = now()
|
||
WHERE id = $1
|
||
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist
|
||
`
|
||
|
||
// Explicit NULL-clear for connector_service_allowlist; see
|
||
// ClearAgentComposioToolkitAllowlist for why COALESCE cannot do this.
|
||
func (q *Queries) ClearAgentConnectorServiceAllowlist(ctx context.Context, id pgtype.UUID) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, clearAgentConnectorServiceAllowlist, id)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const clearAgentMcpConfig = `-- name: ClearAgentMcpConfig :one
|
||
UPDATE agent SET mcp_config = NULL, updated_at = now()
|
||
WHERE id = $1
|
||
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist
|
||
`
|
||
|
||
func (q *Queries) ClearAgentMcpConfig(ctx context.Context, id pgtype.UUID) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, clearAgentMcpConfig, id)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const clearAgentServiceTier = `-- name: ClearAgentServiceTier :one
|
||
UPDATE agent SET service_tier = NULL, updated_at = now()
|
||
WHERE id = $1
|
||
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist
|
||
`
|
||
|
||
// Explicit NULL-clear for service_tier. COALESCE-based UpdateAgent cannot
|
||
// set the column back to NULL, so the API routes "Runtime default" here.
|
||
func (q *Queries) ClearAgentServiceTier(ctx context.Context, id pgtype.UUID) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, clearAgentServiceTier, id)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const clearAgentThinkingLevel = `-- name: ClearAgentThinkingLevel :one
|
||
UPDATE agent SET thinking_level = NULL, updated_at = now()
|
||
WHERE id = $1
|
||
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist
|
||
`
|
||
|
||
// Explicit NULL-clear for thinking_level. COALESCE-based UpdateAgent cannot
|
||
// set the column back to NULL, so the API layer routes "user picked Default"
|
||
// through this dedicated query.
|
||
func (q *Queries) ClearAgentThinkingLevel(ctx context.Context, id pgtype.UUID) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, clearAgentThinkingLevel, id)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const clearTaskConnectorHandles = `-- name: ClearTaskConnectorHandles :exec
|
||
UPDATE agent_task_queue SET runtime_connector_handles = NULL
|
||
WHERE id = $1
|
||
`
|
||
|
||
// Called after a task's per-provider resources were released.
|
||
func (q *Queries) ClearTaskConnectorHandles(ctx context.Context, id pgtype.UUID) error {
|
||
_, err := q.db.Exec(ctx, clearTaskConnectorHandles, id)
|
||
return err
|
||
}
|
||
|
||
const completeAgentTask = `-- name: CompleteAgentTask :one
|
||
UPDATE agent_task_queue
|
||
SET status = 'completed', completed_at = now(), result = $2,
|
||
session_id = CASE WHEN $5 THEN NULL ELSE $3 END,
|
||
work_dir = $4,
|
||
durable_work_dir = COALESCE($6, durable_work_dir),
|
||
branch_name = COALESCE($7, branch_name),
|
||
session_rollout_missing = $5,
|
||
retired_session_id = COALESCE($8, retired_session_id),
|
||
prepare_lease_expires_at = NULL
|
||
WHERE id = $1 AND status = 'running'
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type CompleteAgentTaskParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
Result []byte `json:"result"`
|
||
SessionID pgtype.Text `json:"session_id"`
|
||
WorkDir pgtype.Text `json:"work_dir"`
|
||
SessionRolloutMissing bool `json:"session_rollout_missing"`
|
||
DurableWorkDir pgtype.Text `json:"durable_work_dir"`
|
||
BranchName pgtype.Text `json:"branch_name"`
|
||
RetiredSessionID pgtype.Text `json:"retired_session_id"`
|
||
}
|
||
|
||
// session_rollout_missing (MUL-5305): when true the daemon withheld this task's
|
||
// Codex session because its rollout was never written to the store. Forcing
|
||
// session_id NULL and flagging the row happen in THIS terminal transaction so an
|
||
// auto-retry created and woken by the same commit can never observe the bad
|
||
// pointer or a missing gap flag.
|
||
//
|
||
// retired_session_id (GH #6066): a session this run abandoned as unresumable.
|
||
// Recorded even on the completed path, because that is exactly the case the
|
||
// resume lookups could not see — a fresh-session retry that SUCCEEDS still has
|
||
// to retire the transcript it retried away from, or an older completed row
|
||
// pointing at the same id resurrects it on the next run.
|
||
func (q *Queries) CompleteAgentTask(ctx context.Context, arg CompleteAgentTaskParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, completeAgentTask,
|
||
arg.ID,
|
||
arg.Result,
|
||
arg.SessionID,
|
||
arg.WorkDir,
|
||
arg.SessionRolloutMissing,
|
||
arg.DurableWorkDir,
|
||
arg.BranchName,
|
||
arg.RetiredSessionID,
|
||
)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const countDelegatedFailureRecoveryTasks = `-- name: CountDelegatedFailureRecoveryTasks :one
|
||
SELECT count(*)
|
||
FROM agent_task_queue
|
||
WHERE trigger_evidence_kind = 'delegated_failure'
|
||
AND trigger_evidence_ref_id = $1
|
||
`
|
||
|
||
// Counts dedicated coordinator wakeups for one failed delegated task. Merged
|
||
// recovery signals do not create a new row and therefore do not consume an
|
||
// automatic attempt until they need a standalone successor.
|
||
func (q *Queries) CountDelegatedFailureRecoveryTasks(ctx context.Context, failedTaskID pgtype.UUID) (int64, error) {
|
||
row := q.db.QueryRow(ctx, countDelegatedFailureRecoveryTasks, failedTaskID)
|
||
var count int64
|
||
err := row.Scan(&count)
|
||
return count, err
|
||
}
|
||
|
||
const countRunningTasks = `-- name: CountRunningTasks :one
|
||
SELECT count(*) FROM agent_task_queue
|
||
WHERE agent_id = $1 AND status IN ('dispatched', 'running', 'waiting_local_directory')
|
||
`
|
||
|
||
// waiting_local_directory remains capacity-bearing until resume admission has
|
||
// an atomic reservation/CAS gate. Consequently a waiter-only agent is reported
|
||
// idle by RefreshAgentStatusFromTasks but still cannot claim additional work;
|
||
// removing it here alone could exceed max_concurrent_tasks when it resumes.
|
||
func (q *Queries) CountRunningTasks(ctx context.Context, agentID pgtype.UUID) (int64, error) {
|
||
row := q.db.QueryRow(ctx, countRunningTasks, agentID)
|
||
var count int64
|
||
err := row.Scan(&count)
|
||
return count, err
|
||
}
|
||
|
||
const createAgent = `-- name: CreateAgent :one
|
||
INSERT INTO agent (
|
||
workspace_id, name, description, avatar_url, runtime_mode,
|
||
runtime_config, runtime_id, visibility, max_concurrent_tasks, owner_id,
|
||
instructions, custom_env, custom_args, mcp_config, model, thinking_level,
|
||
service_tier, conversation_starters,
|
||
composio_toolkit_allowlist, permission_mode,
|
||
connector_service_allowlist
|
||
) VALUES (
|
||
$1, $2, $3, $4, $5,
|
||
$6, $7, $8, $9, $10,
|
||
$11, $12, $13, $14, $15, $16,
|
||
$17, COALESCE($18::jsonb, '[]'::jsonb),
|
||
$19::text[],
|
||
COALESCE($20, 'private'),
|
||
$21::text[]
|
||
)
|
||
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist
|
||
`
|
||
|
||
type CreateAgentParams struct {
|
||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||
Name string `json:"name"`
|
||
Description string `json:"description"`
|
||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||
RuntimeMode string `json:"runtime_mode"`
|
||
RuntimeConfig []byte `json:"runtime_config"`
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
Visibility string `json:"visibility"`
|
||
MaxConcurrentTasks int32 `json:"max_concurrent_tasks"`
|
||
OwnerID pgtype.UUID `json:"owner_id"`
|
||
Instructions string `json:"instructions"`
|
||
CustomEnv []byte `json:"custom_env"`
|
||
CustomArgs []byte `json:"custom_args"`
|
||
McpConfig []byte `json:"mcp_config"`
|
||
Model pgtype.Text `json:"model"`
|
||
ThinkingLevel pgtype.Text `json:"thinking_level"`
|
||
ServiceTier pgtype.Text `json:"service_tier"`
|
||
ConversationStarters []byte `json:"conversation_starters"`
|
||
ComposioToolkitAllowlist []string `json:"composio_toolkit_allowlist"`
|
||
PermissionMode interface{} `json:"permission_mode"`
|
||
ConnectorServiceAllowlist []string `json:"connector_service_allowlist"`
|
||
}
|
||
|
||
func (q *Queries) CreateAgent(ctx context.Context, arg CreateAgentParams) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, createAgent,
|
||
arg.WorkspaceID,
|
||
arg.Name,
|
||
arg.Description,
|
||
arg.AvatarUrl,
|
||
arg.RuntimeMode,
|
||
arg.RuntimeConfig,
|
||
arg.RuntimeID,
|
||
arg.Visibility,
|
||
arg.MaxConcurrentTasks,
|
||
arg.OwnerID,
|
||
arg.Instructions,
|
||
arg.CustomEnv,
|
||
arg.CustomArgs,
|
||
arg.McpConfig,
|
||
arg.Model,
|
||
arg.ThinkingLevel,
|
||
arg.ServiceTier,
|
||
arg.ConversationStarters,
|
||
arg.ComposioToolkitAllowlist,
|
||
arg.PermissionMode,
|
||
arg.ConnectorServiceAllowlist,
|
||
)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const createAgentBuilder = `-- name: CreateAgentBuilder :one
|
||
INSERT INTO agent (
|
||
workspace_id, name, description, runtime_mode, runtime_config, runtime_id,
|
||
visibility, permission_mode, max_concurrent_tasks, owner_id, instructions,
|
||
custom_env, custom_args, model, kind, system_key
|
||
) VALUES (
|
||
$1, $2, '', $3, '{}'::jsonb, $4,
|
||
'private', 'private', 1, $5, $6,
|
||
'{}'::jsonb, '[]'::jsonb, $7, 'system', $8
|
||
)
|
||
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist
|
||
`
|
||
|
||
type CreateAgentBuilderParams struct {
|
||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||
Name string `json:"name"`
|
||
RuntimeMode string `json:"runtime_mode"`
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
OwnerID pgtype.UUID `json:"owner_id"`
|
||
Instructions string `json:"instructions"`
|
||
Model pgtype.Text `json:"model"`
|
||
SystemKey pgtype.Text `json:"system_key"`
|
||
}
|
||
|
||
// One hidden builder agent per creation session. Keeping the execution carrier
|
||
// session-scoped freezes its model/runtime configuration when multiple builder
|
||
// flows are open concurrently, while `kind = 'system'` keeps it out of normal
|
||
// agent lists and assignment surfaces.
|
||
func (q *Queries) CreateAgentBuilder(ctx context.Context, arg CreateAgentBuilderParams) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, createAgentBuilder,
|
||
arg.WorkspaceID,
|
||
arg.Name,
|
||
arg.RuntimeMode,
|
||
arg.RuntimeID,
|
||
arg.OwnerID,
|
||
arg.Instructions,
|
||
arg.Model,
|
||
arg.SystemKey,
|
||
)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const createAgentTask = `-- name: CreateAgentTask :one
|
||
INSERT INTO agent_task_queue (
|
||
agent_id, runtime_id, issue_id, status, priority, trigger_comment_id,
|
||
coalesced_comment_ids, trigger_summary, force_fresh_session, is_leader_task, handoff_note,
|
||
squad_id, context, originator_user_id, accountable_user_id, runtime_mcp_overlay, runtime_connected_apps, runtime_connector_handles,
|
||
originator_source, delegated_from_task_id, rule_version_id, rerun_of_task_id, trigger_evidence_kind, trigger_evidence_ref_id,
|
||
workflow_step, id
|
||
)
|
||
SELECT
|
||
$1, $2, $3, 'queued', $4, $5,
|
||
COALESCE($6::uuid[], '{}'),
|
||
$7,
|
||
COALESCE($8::boolean, FALSE),
|
||
COALESCE($9::boolean, FALSE),
|
||
$10,
|
||
$11,
|
||
CASE
|
||
WHEN COALESCE($12::text, '') <> ''
|
||
THEN jsonb_build_object('head_sha', $12::text)
|
||
ELSE NULL
|
||
END,
|
||
$13,
|
||
$14,
|
||
$15,
|
||
$16, $17,
|
||
$18,
|
||
$19,
|
||
$20,
|
||
$21,
|
||
$22,
|
||
$23,
|
||
-- The issue's status as this task is queued: the workflow step it works on (MUL-7420).
|
||
(SELECT i.status FROM issue i WHERE i.id = $3),
|
||
COALESCE($24::uuid, gen_random_uuid())
|
||
WHERE lock_task_owner_rows($1, $3, $2)
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type CreateAgentTaskParams struct {
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
Priority int32 `json:"priority"`
|
||
TriggerCommentID pgtype.UUID `json:"trigger_comment_id"`
|
||
CoalescedCommentIds []pgtype.UUID `json:"coalesced_comment_ids"`
|
||
TriggerSummary pgtype.Text `json:"trigger_summary"`
|
||
ForceFreshSession pgtype.Bool `json:"force_fresh_session"`
|
||
IsLeaderTask pgtype.Bool `json:"is_leader_task"`
|
||
HandoffNote pgtype.Text `json:"handoff_note"`
|
||
SquadID pgtype.UUID `json:"squad_id"`
|
||
HeadSha pgtype.Text `json:"head_sha"`
|
||
OriginatorUserID pgtype.UUID `json:"originator_user_id"`
|
||
AccountableUserID pgtype.UUID `json:"accountable_user_id"`
|
||
RuntimeMcpOverlay []byte `json:"runtime_mcp_overlay"`
|
||
RuntimeConnectedApps []byte `json:"runtime_connected_apps"`
|
||
RuntimeConnectorHandles []byte `json:"runtime_connector_handles"`
|
||
OriginatorSource pgtype.Text `json:"originator_source"`
|
||
DelegatedFromTaskID pgtype.UUID `json:"delegated_from_task_id"`
|
||
RuleVersionID pgtype.UUID `json:"rule_version_id"`
|
||
RerunOfTaskID pgtype.UUID `json:"rerun_of_task_id"`
|
||
TriggerEvidenceKind pgtype.Text `json:"trigger_evidence_kind"`
|
||
TriggerEvidenceRefID pgtype.UUID `json:"trigger_evidence_ref_id"`
|
||
ID pgtype.UUID `json:"id"`
|
||
}
|
||
|
||
// Fenced against workspace teardown: lock_task_owner_rows (migration 284)
|
||
// locks the owners' workspace rows in the writer's own transaction and returns
|
||
// false once they are gone, so this statement writes no row instead of stranding
|
||
// a task in a workspace that has just been deleted (MUL-5999).
|
||
// head_sha stamps the commit under review into the task's context JSONB so the
|
||
// reviewer-loop dedup (HasPendingTaskForIssueAndAgent) can tell a pending run
|
||
// against an OLD head apart from a fresh request against a NEW head (TEN-356).
|
||
// Empty/absent head_sha leaves context NULL, preserving pre-TEN-356 behavior for
|
||
// issues with no linked PR. Issue-linked tasks never hit quick-create context
|
||
// parsing (parseQuickCreateContext short-circuits on IssueID.Valid), so this
|
||
// key rides harmlessly alongside.
|
||
// id is minted by the application as a UUIDv7 (pkg/dbid) so consecutive
|
||
// enqueues cluster in a narrow contiguous primary-key range instead of
|
||
// scattering across the B-tree. On a table with existing v4 ids, that range
|
||
// is not necessarily the tree's right edge.
|
||
// COALESCE keeps the column's gen_random_uuid() default reachable, so a caller
|
||
// that passes no id still inserts — it just gets a random v4, exactly as before.
|
||
// The same pattern is used by every INSERT listed in pkg/dbid's write table.
|
||
func (q *Queries) CreateAgentTask(ctx context.Context, arg CreateAgentTaskParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, createAgentTask,
|
||
arg.AgentID,
|
||
arg.RuntimeID,
|
||
arg.IssueID,
|
||
arg.Priority,
|
||
arg.TriggerCommentID,
|
||
arg.CoalescedCommentIds,
|
||
arg.TriggerSummary,
|
||
arg.ForceFreshSession,
|
||
arg.IsLeaderTask,
|
||
arg.HandoffNote,
|
||
arg.SquadID,
|
||
arg.HeadSha,
|
||
arg.OriginatorUserID,
|
||
arg.AccountableUserID,
|
||
arg.RuntimeMcpOverlay,
|
||
arg.RuntimeConnectedApps,
|
||
arg.RuntimeConnectorHandles,
|
||
arg.OriginatorSource,
|
||
arg.DelegatedFromTaskID,
|
||
arg.RuleVersionID,
|
||
arg.RerunOfTaskID,
|
||
arg.TriggerEvidenceKind,
|
||
arg.TriggerEvidenceRefID,
|
||
arg.ID,
|
||
)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const createDeferredChannelIssueTask = `-- name: CreateDeferredChannelIssueTask :one
|
||
INSERT INTO agent_task_queue (
|
||
agent_id, runtime_id, issue_id, status, priority, trigger_comment_id,
|
||
coalesced_comment_ids, trigger_summary, force_fresh_session, is_leader_task, handoff_note,
|
||
squad_id, context, originator_user_id, accountable_user_id, runtime_mcp_overlay, runtime_connected_apps, runtime_connector_handles,
|
||
originator_source, delegated_from_task_id, rule_version_id, rerun_of_task_id,
|
||
trigger_evidence_kind, trigger_evidence_ref_id, fire_at,
|
||
workflow_step, id
|
||
)
|
||
SELECT
|
||
$1, $2, $3, 'deferred', $4, $5,
|
||
COALESCE($6::uuid[], '{}'),
|
||
$7,
|
||
COALESCE($8::boolean, FALSE),
|
||
COALESCE($9::boolean, FALSE),
|
||
$10,
|
||
$11,
|
||
jsonb_strip_nulls(jsonb_build_object(
|
||
'head_sha', NULLIF(COALESCE($12::text, ''), ''),
|
||
'channel_issue_media_pending', TRUE
|
||
)),
|
||
$13,
|
||
$14,
|
||
$15,
|
||
$16, $17,
|
||
$18,
|
||
$19,
|
||
$20,
|
||
$21,
|
||
$22,
|
||
$23,
|
||
$24,
|
||
-- The issue's status as this task is queued: the workflow step it works on (MUL-7420).
|
||
(SELECT i.status FROM issue i WHERE i.id = $3),
|
||
COALESCE($25::uuid, gen_random_uuid())
|
||
WHERE lock_task_owner_rows($1, $3, $2)
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type CreateDeferredChannelIssueTaskParams struct {
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
Priority int32 `json:"priority"`
|
||
TriggerCommentID pgtype.UUID `json:"trigger_comment_id"`
|
||
CoalescedCommentIds []pgtype.UUID `json:"coalesced_comment_ids"`
|
||
TriggerSummary pgtype.Text `json:"trigger_summary"`
|
||
ForceFreshSession pgtype.Bool `json:"force_fresh_session"`
|
||
IsLeaderTask pgtype.Bool `json:"is_leader_task"`
|
||
HandoffNote pgtype.Text `json:"handoff_note"`
|
||
SquadID pgtype.UUID `json:"squad_id"`
|
||
HeadSha pgtype.Text `json:"head_sha"`
|
||
OriginatorUserID pgtype.UUID `json:"originator_user_id"`
|
||
AccountableUserID pgtype.UUID `json:"accountable_user_id"`
|
||
RuntimeMcpOverlay []byte `json:"runtime_mcp_overlay"`
|
||
RuntimeConnectedApps []byte `json:"runtime_connected_apps"`
|
||
RuntimeConnectorHandles []byte `json:"runtime_connector_handles"`
|
||
OriginatorSource pgtype.Text `json:"originator_source"`
|
||
DelegatedFromTaskID pgtype.UUID `json:"delegated_from_task_id"`
|
||
RuleVersionID pgtype.UUID `json:"rule_version_id"`
|
||
RerunOfTaskID pgtype.UUID `json:"rerun_of_task_id"`
|
||
TriggerEvidenceKind pgtype.Text `json:"trigger_evidence_kind"`
|
||
TriggerEvidenceRefID pgtype.UUID `json:"trigger_evidence_ref_id"`
|
||
FireAt pgtype.Timestamptz `json:"fire_at"`
|
||
ID pgtype.UUID `json:"id"`
|
||
}
|
||
|
||
// Fenced against workspace teardown: lock_task_owner_rows (migration 284)
|
||
// locks the owners' workspace rows in the writer's own transaction and returns
|
||
// false once they are gone, so this statement writes no row instead of stranding
|
||
// a task in a workspace that has just been deleted (MUL-5999).
|
||
// Channel /issue media resolves after issue creation. Persist the assigned
|
||
// issue task up front for crash safety, but keep it inert until attachment
|
||
// binding settles or the fire_at fallback is promoted by the normal sweeper.
|
||
func (q *Queries) CreateDeferredChannelIssueTask(ctx context.Context, arg CreateDeferredChannelIssueTaskParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, createDeferredChannelIssueTask,
|
||
arg.AgentID,
|
||
arg.RuntimeID,
|
||
arg.IssueID,
|
||
arg.Priority,
|
||
arg.TriggerCommentID,
|
||
arg.CoalescedCommentIds,
|
||
arg.TriggerSummary,
|
||
arg.ForceFreshSession,
|
||
arg.IsLeaderTask,
|
||
arg.HandoffNote,
|
||
arg.SquadID,
|
||
arg.HeadSha,
|
||
arg.OriginatorUserID,
|
||
arg.AccountableUserID,
|
||
arg.RuntimeMcpOverlay,
|
||
arg.RuntimeConnectedApps,
|
||
arg.RuntimeConnectorHandles,
|
||
arg.OriginatorSource,
|
||
arg.DelegatedFromTaskID,
|
||
arg.RuleVersionID,
|
||
arg.RerunOfTaskID,
|
||
arg.TriggerEvidenceKind,
|
||
arg.TriggerEvidenceRefID,
|
||
arg.FireAt,
|
||
arg.ID,
|
||
)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const createManualQuickCreateRetryTask = `-- name: CreateManualQuickCreateRetryTask :one
|
||
INSERT INTO agent_task_queue (
|
||
agent_id, runtime_id, status, priority, context,
|
||
force_fresh_session, is_leader_task, squad_id,
|
||
originator_user_id, accountable_user_id,
|
||
runtime_mcp_overlay, runtime_connected_apps, runtime_connector_handles,
|
||
originator_source, rerun_of_task_id, id
|
||
)
|
||
SELECT
|
||
p.agent_id, p.runtime_id, 'queued', p.priority, p.context,
|
||
TRUE, p.is_leader_task, p.squad_id,
|
||
$1, $1,
|
||
$2, $3, $4,
|
||
'direct_human', p.id, $5
|
||
FROM agent_task_queue p
|
||
WHERE p.id = $6
|
||
AND p.status = 'failed'
|
||
AND p.issue_id IS NULL
|
||
AND p.chat_session_id IS NULL
|
||
AND p.autopilot_run_id IS NULL
|
||
AND lock_task_owner_rows(p.agent_id, p.issue_id, p.runtime_id)
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type CreateManualQuickCreateRetryTaskParams struct {
|
||
ActorUserID pgtype.UUID `json:"actor_user_id"`
|
||
RuntimeMcpOverlay []byte `json:"runtime_mcp_overlay"`
|
||
RuntimeConnectedApps []byte `json:"runtime_connected_apps"`
|
||
RuntimeConnectorHandles []byte `json:"runtime_connector_handles"`
|
||
NewTaskID pgtype.UUID `json:"new_task_id"`
|
||
SourceTaskID pgtype.UUID `json:"source_task_id"`
|
||
}
|
||
|
||
// A human retry of an issue-less quick-create is a new direct_human run, not
|
||
// an automatic retry. It preserves the immutable quick-create context JSON
|
||
// (including source_context_id), but deliberately starts a fresh session and
|
||
// records rerun_of_task_id so attribution/reporting can distinguish the human
|
||
// action from CreateRetryTask's retry_of_task_id lineage.
|
||
//
|
||
// The caller locks and transfers the pending issue_source_context in the same
|
||
// transaction. lock_task_owner_rows provides the usual workspace-teardown
|
||
// fence; a deleted workspace therefore yields pgx.ErrNoRows and no task.
|
||
func (q *Queries) CreateManualQuickCreateRetryTask(ctx context.Context, arg CreateManualQuickCreateRetryTaskParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, createManualQuickCreateRetryTask,
|
||
arg.ActorUserID,
|
||
arg.RuntimeMcpOverlay,
|
||
arg.RuntimeConnectedApps,
|
||
arg.RuntimeConnectorHandles,
|
||
arg.NewTaskID,
|
||
arg.SourceTaskID,
|
||
)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const createQuickCreateTask = `-- name: CreateQuickCreateTask :one
|
||
INSERT INTO agent_task_queue (
|
||
agent_id, runtime_id, issue_id, status, priority, context, originator_user_id,
|
||
accountable_user_id, runtime_mcp_overlay, runtime_connected_apps, runtime_connector_handles,
|
||
originator_source, trigger_evidence_kind, trigger_evidence_ref_id,
|
||
id
|
||
)
|
||
SELECT
|
||
$1, $2, NULL, 'queued', $3, $4,
|
||
$5,
|
||
$6,
|
||
$7,
|
||
$8, $9,
|
||
$10,
|
||
$11,
|
||
$12,
|
||
COALESCE($13::uuid, gen_random_uuid())
|
||
WHERE lock_task_owner_rows($1, NULL, $2)
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type CreateQuickCreateTaskParams struct {
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
Priority int32 `json:"priority"`
|
||
Context []byte `json:"context"`
|
||
OriginatorUserID pgtype.UUID `json:"originator_user_id"`
|
||
AccountableUserID pgtype.UUID `json:"accountable_user_id"`
|
||
RuntimeMcpOverlay []byte `json:"runtime_mcp_overlay"`
|
||
RuntimeConnectedApps []byte `json:"runtime_connected_apps"`
|
||
RuntimeConnectorHandles []byte `json:"runtime_connector_handles"`
|
||
OriginatorSource pgtype.Text `json:"originator_source"`
|
||
TriggerEvidenceKind pgtype.Text `json:"trigger_evidence_kind"`
|
||
TriggerEvidenceRefID pgtype.UUID `json:"trigger_evidence_ref_id"`
|
||
ID pgtype.UUID `json:"id"`
|
||
}
|
||
|
||
// Fenced against workspace teardown: lock_task_owner_rows (migration 284)
|
||
// locks the owners' workspace rows in the writer's own transaction and returns
|
||
// false once they are gone, so this statement writes no row instead of stranding
|
||
// a task in a workspace that has just been deleted (MUL-5999).
|
||
// Quick-create tasks have no issue / chat / autopilot link; the entire job
|
||
// description (prompt, requester, workspace) lives in context JSONB. The
|
||
// daemon detects this variant via context.type == "quick_create".
|
||
// The requester who opened the quick-create modal is a direct_human originator
|
||
// and accountable; attribution provenance is stamped so this path is not a
|
||
// NULL-source enqueue bypass (MUL-4302 §2).
|
||
func (q *Queries) CreateQuickCreateTask(ctx context.Context, arg CreateQuickCreateTaskParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, createQuickCreateTask,
|
||
arg.AgentID,
|
||
arg.RuntimeID,
|
||
arg.Priority,
|
||
arg.Context,
|
||
arg.OriginatorUserID,
|
||
arg.AccountableUserID,
|
||
arg.RuntimeMcpOverlay,
|
||
arg.RuntimeConnectedApps,
|
||
arg.RuntimeConnectorHandles,
|
||
arg.OriginatorSource,
|
||
arg.TriggerEvidenceKind,
|
||
arg.TriggerEvidenceRefID,
|
||
arg.ID,
|
||
)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const createRetryTask = `-- name: CreateRetryTask :one
|
||
INSERT INTO agent_task_queue (
|
||
agent_id, runtime_id, issue_id, chat_session_id, autopilot_run_id,
|
||
status, priority, trigger_comment_id, coalesced_comment_ids, trigger_summary, context,
|
||
session_id, work_dir,
|
||
attempt, max_attempts, parent_task_id, force_fresh_session, is_leader_task,
|
||
squad_id, originator_user_id, accountable_user_id, runtime_mcp_overlay, runtime_connected_apps, runtime_connector_handles,
|
||
originator_source, delegated_from_task_id, rule_version_id,
|
||
trigger_evidence_kind, trigger_evidence_ref_id, retry_of_task_id,
|
||
chat_input_task_id, fire_at,
|
||
channel_context_revision, handoff_note, workflow_step, id
|
||
)
|
||
SELECT
|
||
p.agent_id, p.runtime_id, p.issue_id, p.chat_session_id, p.autopilot_run_id,
|
||
CASE WHEN $2::timestamptz IS NOT NULL THEN 'deferred' ELSE 'queued' END,
|
||
CASE WHEN p.chat_session_id IS NOT NULL THEN GREATEST(p.priority, 3) ELSE p.priority END,
|
||
p.trigger_comment_id, p.coalesced_comment_ids, p.trigger_summary, p.context,
|
||
CASE WHEN p.failure_reason IS NOT DISTINCT FROM 'codex_semantic_inactivity' THEN NULL ELSE p.session_id END,
|
||
p.work_dir,
|
||
p.attempt + 1, COALESCE($3::int, p.max_attempts), p.id,
|
||
p.failure_reason IS NOT DISTINCT FROM 'codex_semantic_inactivity',
|
||
p.is_leader_task,
|
||
p.squad_id,
|
||
p.originator_user_id,
|
||
p.accountable_user_id,
|
||
$4,
|
||
$5, $6,
|
||
p.originator_source, p.delegated_from_task_id, p.rule_version_id,
|
||
p.trigger_evidence_kind, p.trigger_evidence_ref_id, p.id,
|
||
p.chat_input_task_id, $2,
|
||
p.channel_context_revision,
|
||
-- Carried for a wakeup's prompt and a workflow handoff's brief (MUL-7420);
|
||
-- rows from before workflow_step existed may hold a retired assignment note.
|
||
CASE WHEN p.context->>'wakeup_id' IS NOT NULL OR p.workflow_step IS NOT NULL THEN p.handoff_note END,
|
||
-- A retry continues its parent's work, so it keeps the parent's step (MUL-7420).
|
||
p.workflow_step,
|
||
-- Named new_task_id, not id: $1 above is the PARENT task's id.
|
||
COALESCE($7::uuid, gen_random_uuid())
|
||
FROM agent_task_queue p
|
||
WHERE p.id = $1
|
||
AND lock_task_owner_rows(p.agent_id, p.issue_id, p.runtime_id)
|
||
ON CONFLICT (issue_id, agent_id, (COALESCE(comment_thread_id, '00000000-0000-0000-0000-000000000000'::uuid))) WHERE status IN ('queued', 'dispatched')
|
||
OR (status = 'deferred' AND context->>'channel_issue_media_pending' = 'true')
|
||
DO NOTHING
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type CreateRetryTaskParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
FireAt pgtype.Timestamptz `json:"fire_at"`
|
||
MaxAttempts pgtype.Int4 `json:"max_attempts"`
|
||
RuntimeMcpOverlay []byte `json:"runtime_mcp_overlay"`
|
||
RuntimeConnectedApps []byte `json:"runtime_connected_apps"`
|
||
RuntimeConnectorHandles []byte `json:"runtime_connector_handles"`
|
||
NewTaskID pgtype.UUID `json:"new_task_id"`
|
||
}
|
||
|
||
// Clone an attempt with inherited attribution, planned comments, and input-batch
|
||
// identity. The child earns its own delivered-comment receipt at claim time.
|
||
// Preserve a NULL chat_input_task_id for legacy/channel parents rather than
|
||
// promoting untagged history to the task-owned input path.
|
||
//
|
||
// Keep work_dir for every retry, but clear resume context for unsafe sessions.
|
||
// Keep failure predicates aligned with resumeUnsafeFailureReason and resume lookups.
|
||
// Composio uses the agent owner's connection; originator remains audit provenance.
|
||
//
|
||
// Owner-row locks fence workspace teardown. Slot conflicts yield no row instead
|
||
// of aborting the caller's failure transaction; pgx.ErrNoRows means commit the
|
||
// parent transition without a retry. Pending human reruns retain their slot.
|
||
//
|
||
// Chat retry priority keeps older input ahead of newer turns. A non-NULL fire_at
|
||
// defers dispatch until promotion. max_attempts optionally persists the effective
|
||
// ceiling; it does not re-enable tasks whose retry budget was disabled.
|
||
func (q *Queries) CreateRetryTask(ctx context.Context, arg CreateRetryTaskParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, createRetryTask,
|
||
arg.ID,
|
||
arg.FireAt,
|
||
arg.MaxAttempts,
|
||
arg.RuntimeMcpOverlay,
|
||
arg.RuntimeConnectedApps,
|
||
arg.RuntimeConnectorHandles,
|
||
arg.NewTaskID,
|
||
)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const createSystemUserAgent = `-- name: CreateSystemUserAgent :one
|
||
INSERT INTO agent (
|
||
workspace_id, name, description, avatar_url, runtime_mode, runtime_config,
|
||
runtime_id, model, visibility, permission_mode, max_concurrent_tasks,
|
||
owner_id, instructions, custom_env, custom_args, kind, system_key
|
||
) VALUES (
|
||
$1, $2, $3, $4, $5, '{}'::jsonb,
|
||
$6, $7, $8, $9, $10,
|
||
$11, '', '{}'::jsonb, '[]'::jsonb, 'user', $12
|
||
)
|
||
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist
|
||
`
|
||
|
||
type CreateSystemUserAgentParams struct {
|
||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||
Name string `json:"name"`
|
||
Description string `json:"description"`
|
||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||
RuntimeMode string `json:"runtime_mode"`
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
Model pgtype.Text `json:"model"`
|
||
Visibility string `json:"visibility"`
|
||
PermissionMode string `json:"permission_mode"`
|
||
MaxConcurrentTasks int32 `json:"max_concurrent_tasks"`
|
||
OwnerID pgtype.UUID `json:"owner_id"`
|
||
SystemKey pgtype.Text `json:"system_key"`
|
||
}
|
||
|
||
// Creates a product-defined agent that members can still see, chat with, and
|
||
// assign issues to. Deliberately kind='user': kind='system' hides the row from
|
||
// agent lists and assignment surfaces and hard deletes it with its runtime.
|
||
// Every product-owned field is a server constant; instructions stays empty
|
||
// because the system half ships with the binary and is layered in at claim
|
||
// time, leaving this column free for the workspace's own notes.
|
||
//
|
||
// CONTRACT: call only while holding the per-workspace mika advisory lock. The
|
||
// one-per-workspace invariant rests on a check inside that lock, not on a
|
||
// unique index — migration 172's index keys on (workspace_id, owner_id,
|
||
// runtime_id, system_key), so two different owners or runtimes are distinct
|
||
// tuples and would both insert.
|
||
func (q *Queries) CreateSystemUserAgent(ctx context.Context, arg CreateSystemUserAgentParams) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, createSystemUserAgent,
|
||
arg.WorkspaceID,
|
||
arg.Name,
|
||
arg.Description,
|
||
arg.AvatarUrl,
|
||
arg.RuntimeMode,
|
||
arg.RuntimeID,
|
||
arg.Model,
|
||
arg.Visibility,
|
||
arg.PermissionMode,
|
||
arg.MaxConcurrentTasks,
|
||
arg.OwnerID,
|
||
arg.SystemKey,
|
||
)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const deleteSystemAgentByID = `-- name: DeleteSystemAgentByID :exec
|
||
DELETE FROM agent
|
||
WHERE id = $1 AND kind = 'system' AND system_key LIKE 'agent_builder:%'
|
||
`
|
||
|
||
// Builder sessions own their hidden execution agent. Deleting the session
|
||
// removes that carrier and its task rows; the kind guard prevents this cleanup
|
||
// path from ever deleting a user-authored agent.
|
||
func (q *Queries) DeleteSystemAgentByID(ctx context.Context, id pgtype.UUID) error {
|
||
_, err := q.db.Exec(ctx, deleteSystemAgentByID, id)
|
||
return err
|
||
}
|
||
|
||
const deleteUnstartedQuickCreateRetryTask = `-- name: DeleteUnstartedQuickCreateRetryTask :execrows
|
||
DELETE FROM agent_task_queue
|
||
WHERE id = $1
|
||
AND status IN ('queued', 'deferred')
|
||
AND issue_id IS NULL
|
||
AND chat_session_id IS NULL
|
||
AND autopilot_run_id IS NULL
|
||
`
|
||
|
||
// FailTask creates an automatic retry in the same transaction as the parent
|
||
// terminal write. An issue-less source-context retry has no pending-slot
|
||
// unique key, so a competing retry or an already-attached context can make
|
||
// the subsequent attach-authority transfer lose after this row was inserted.
|
||
// Remove only that still-uncommitted child and let the parent's failure commit.
|
||
func (q *Queries) DeleteUnstartedQuickCreateRetryTask(ctx context.Context, taskID pgtype.UUID) (int64, error) {
|
||
result, err := q.db.Exec(ctx, deleteUnstartedQuickCreateRetryTask, taskID)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return result.RowsAffected(), nil
|
||
}
|
||
|
||
const expireStaleQueuedTasks = `-- name: ExpireStaleQueuedTasks :many
|
||
WITH victims AS (
|
||
SELECT id FROM agent_task_queue
|
||
WHERE status = 'queued' AND context->>'wakeup_id' IS NULL
|
||
AND created_at < now() - make_interval(secs => $1::double precision)
|
||
AND (
|
||
runtime_id IS NULL
|
||
OR NOT EXISTS (
|
||
SELECT 1 FROM agent_runtime r WHERE r.id = agent_task_queue.runtime_id
|
||
)
|
||
OR EXISTS (
|
||
SELECT 1 FROM agent_runtime r
|
||
WHERE r.id = agent_task_queue.runtime_id
|
||
AND COALESCE(r.last_seen_at, r.updated_at) <
|
||
now() - make_interval(secs => $1::double precision)
|
||
)
|
||
)
|
||
AND NOT EXISTS (
|
||
SELECT 1 FROM agent_task_queue retry_parent
|
||
WHERE retry_parent.id = agent_task_queue.parent_task_id
|
||
AND retry_parent.failure_reason = 'runtime_offline'
|
||
)
|
||
ORDER BY created_at ASC
|
||
LIMIT $2::int
|
||
FOR UPDATE SKIP LOCKED
|
||
)
|
||
UPDATE agent_task_queue t
|
||
SET status = 'failed',
|
||
completed_at = now(),
|
||
error = 'runtime unavailable while task was queued',
|
||
failure_reason = 'queued_expired',
|
||
prepare_lease_expires_at = NULL
|
||
FROM victims v
|
||
WHERE t.id = v.id
|
||
AND t.status = 'queued'
|
||
AND t.created_at < now() - make_interval(secs => $1::double precision)
|
||
AND (
|
||
t.runtime_id IS NULL
|
||
OR NOT EXISTS (SELECT 1 FROM agent_runtime r WHERE r.id = t.runtime_id)
|
||
OR EXISTS (
|
||
SELECT 1 FROM agent_runtime r
|
||
WHERE r.id = t.runtime_id
|
||
AND COALESCE(r.last_seen_at, r.updated_at) <
|
||
now() - make_interval(secs => $1::double precision)
|
||
)
|
||
)
|
||
AND NOT EXISTS (
|
||
SELECT 1 FROM agent_task_queue retry_parent
|
||
WHERE retry_parent.id = t.parent_task_id
|
||
AND retry_parent.failure_reason = 'runtime_offline'
|
||
)
|
||
RETURNING t.id, t.agent_id, t.issue_id, t.status, t.priority, t.dispatched_at, t.started_at, t.completed_at, t.result, t.error, t.created_at, t.context, t.runtime_id, t.session_id, t.work_dir, t.trigger_comment_id, t.chat_session_id, t.autopilot_run_id, t.attempt, t.max_attempts, t.parent_task_id, t.failure_reason, t.trigger_summary, t.force_fresh_session, t.is_leader_task, t.wait_reason, t.initiator_user_id, t.handoff_note, t.prepare_lease_expires_at, t.squad_id, t.runtime_mcp_overlay, t.escalation_for_task_id, t.fire_at, t.originator_user_id, t.runtime_connected_apps, t.coalesced_comment_ids, t.delivered_comment_ids, t.chat_input_task_id, t.chat_finalize_deferred_at, t.originator_source, t.delegated_from_task_id, t.retry_of_task_id, t.rerun_of_task_id, t.rule_version_id, t.trigger_evidence_kind, t.trigger_evidence_ref_id, t.accountable_user_id, t.session_rollout_missing, t.retired_session_id, t.quick_actions_disabled, t.regenerate_quick_actions_for, t.branch_name, t.durable_work_dir, t.channel_context_revision, t.comment_thread_id, t.cancelled_by_type, t.cancelled_by_id, t.cancelled_by_name, t.issue_snapshot, t.workflow_step, t.runtime_connector_handles
|
||
`
|
||
|
||
type ExpireStaleQueuedTasksParams struct {
|
||
ReconnectGraceSecs float64 `json:"reconnect_grace_secs"`
|
||
MaxPerTick int32 `json:"max_per_tick"`
|
||
}
|
||
|
||
// Expire queued work only when both the task's waiting age and the runtime's
|
||
// heartbeat age exceed reconnect grace. Busy but heartbeating runtimes keep their
|
||
// queues regardless of length. Read heartbeat age directly so a stale online
|
||
// status cannot strand tasks. Missing runtime references are handled defensively.
|
||
//
|
||
// runtime_offline retries have a separate reconnect expiry policy and are exempt.
|
||
// Select bounded victims with SKIP LOCKED, then re-check status and liveness in
|
||
// the update so concurrent claims cannot be overwritten.
|
||
func (q *Queries) ExpireStaleQueuedTasks(ctx context.Context, arg ExpireStaleQueuedTasksParams) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, expireStaleQueuedTasks, arg.ReconnectGraceSecs, arg.MaxPerTick)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const extendAgentTaskPrepareLease = `-- name: ExtendAgentTaskPrepareLease :one
|
||
UPDATE agent_task_queue
|
||
SET prepare_lease_expires_at = now() + make_interval(secs => $3::double precision)
|
||
WHERE id = $1
|
||
AND runtime_id = $2
|
||
AND status IN ('dispatched', 'waiting_local_directory')
|
||
AND started_at IS NULL
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type ExtendAgentTaskPrepareLeaseParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
LeaseSecs float64 `json:"lease_secs"`
|
||
}
|
||
|
||
// Keeps a dispatched task protected while the daemon resolves/cache/materializes
|
||
// startup inputs before StartTask. Once the daemon stops extending this short
|
||
// lease, the stale-dispatched reclaim path can recover the task without waiting
|
||
// for a long global recovery window.
|
||
func (q *Queries) ExtendAgentTaskPrepareLease(ctx context.Context, arg ExtendAgentTaskPrepareLeaseParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, extendAgentTaskPrepareLease, arg.ID, arg.RuntimeID, arg.LeaseSecs)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const failAgentTask = `-- name: FailAgentTask :one
|
||
UPDATE agent_task_queue
|
||
SET status = 'failed',
|
||
completed_at = now(),
|
||
error = $2,
|
||
failure_reason = COALESCE($3, 'agent_error'),
|
||
session_id = CASE WHEN $4 THEN NULL ELSE COALESCE($5, session_id) END,
|
||
work_dir = COALESCE($6, work_dir),
|
||
durable_work_dir = COALESCE($7, durable_work_dir),
|
||
branch_name = COALESCE($8, branch_name),
|
||
session_rollout_missing = $4,
|
||
retired_session_id = COALESCE($9, retired_session_id),
|
||
prepare_lease_expires_at = NULL
|
||
WHERE id = $1 AND status IN ('dispatched', 'running', 'waiting_local_directory')
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type FailAgentTaskParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
Error pgtype.Text `json:"error"`
|
||
FailureReason pgtype.Text `json:"failure_reason"`
|
||
SessionRolloutMissing bool `json:"session_rollout_missing"`
|
||
SessionID pgtype.Text `json:"session_id"`
|
||
WorkDir pgtype.Text `json:"work_dir"`
|
||
DurableWorkDir pgtype.Text `json:"durable_work_dir"`
|
||
BranchName pgtype.Text `json:"branch_name"`
|
||
RetiredSessionID pgtype.Text `json:"retired_session_id"`
|
||
}
|
||
|
||
// Marks a task as failed. session_id and work_dir are merged via COALESCE so
|
||
// if the agent already established a real session before failing (e.g. it
|
||
// crashed mid-conversation, was cancelled, or hit a tool error) the resume
|
||
// pointer is preserved on the task row. The next chat task can then fall
|
||
// back to GetLastChatTaskSession and continue the conversation instead of
|
||
// silently starting over.
|
||
//
|
||
// failure_reason is a coarse classifier consumed by the auto-retry path;
|
||
// 'agent_error' is the safe default when the daemon doesn't supply one.
|
||
//
|
||
// session_rollout_missing (MUL-5305): when true the daemon withheld this task's
|
||
// Codex session (its rollout was missing). Force session_id NULL — overriding
|
||
// the COALESCE that would otherwise preserve a stale mid-flight pin — and flag
|
||
// the row, in the SAME transaction that creates and wakes the auto-retry, so the
|
||
// retry can never claim the bad pointer or miss the continuity gap.
|
||
func (q *Queries) FailAgentTask(ctx context.Context, arg FailAgentTaskParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, failAgentTask,
|
||
arg.ID,
|
||
arg.Error,
|
||
arg.FailureReason,
|
||
arg.SessionRolloutMissing,
|
||
arg.SessionID,
|
||
arg.WorkDir,
|
||
arg.DurableWorkDir,
|
||
arg.BranchName,
|
||
arg.RetiredSessionID,
|
||
)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const failExpiredRuntimeReconnectRetries = `-- name: FailExpiredRuntimeReconnectRetries :many
|
||
WITH victims AS (
|
||
SELECT retry.id
|
||
FROM agent_task_queue retry
|
||
JOIN agent_task_queue parent ON parent.id = retry.parent_task_id
|
||
WHERE retry.status = 'deferred'
|
||
AND retry.fire_at < now() - make_interval(secs => $1::double precision)
|
||
AND parent.failure_reason = 'runtime_offline'
|
||
AND NOT EXISTS (
|
||
SELECT 1 FROM agent_runtime runtime
|
||
WHERE runtime.id = retry.runtime_id
|
||
AND runtime.status = 'online'
|
||
AND COALESCE(runtime.last_seen_at, runtime.updated_at) >=
|
||
now() - make_interval(secs => $2::double precision)
|
||
)
|
||
ORDER BY retry.fire_at, retry.created_at
|
||
LIMIT $3::int
|
||
FOR UPDATE OF retry SKIP LOCKED
|
||
)
|
||
UPDATE agent_task_queue AS retry
|
||
SET status = 'failed',
|
||
completed_at = now(),
|
||
error = 'runtime did not reconnect within the configured grace period',
|
||
failure_reason = 'runtime_reconnect_timeout',
|
||
wait_reason = NULL,
|
||
prepare_lease_expires_at = NULL
|
||
FROM victims
|
||
WHERE retry.id = victims.id
|
||
AND retry.status = 'deferred'
|
||
AND retry.fire_at < now() - make_interval(secs => $1::double precision)
|
||
AND EXISTS (
|
||
SELECT 1 FROM agent_task_queue parent
|
||
WHERE parent.id = retry.parent_task_id
|
||
AND parent.failure_reason = 'runtime_offline'
|
||
)
|
||
AND NOT EXISTS (
|
||
SELECT 1 FROM agent_runtime runtime
|
||
WHERE runtime.id = retry.runtime_id
|
||
AND runtime.status = 'online'
|
||
AND COALESCE(runtime.last_seen_at, runtime.updated_at) >=
|
||
now() - make_interval(secs => $2::double precision)
|
||
)
|
||
RETURNING retry.id, retry.agent_id, retry.issue_id, retry.status, retry.priority, retry.dispatched_at, retry.started_at, retry.completed_at, retry.result, retry.error, retry.created_at, retry.context, retry.runtime_id, retry.session_id, retry.work_dir, retry.trigger_comment_id, retry.chat_session_id, retry.autopilot_run_id, retry.attempt, retry.max_attempts, retry.parent_task_id, retry.failure_reason, retry.trigger_summary, retry.force_fresh_session, retry.is_leader_task, retry.wait_reason, retry.initiator_user_id, retry.handoff_note, retry.prepare_lease_expires_at, retry.squad_id, retry.runtime_mcp_overlay, retry.escalation_for_task_id, retry.fire_at, retry.originator_user_id, retry.runtime_connected_apps, retry.coalesced_comment_ids, retry.delivered_comment_ids, retry.chat_input_task_id, retry.chat_finalize_deferred_at, retry.originator_source, retry.delegated_from_task_id, retry.retry_of_task_id, retry.rerun_of_task_id, retry.rule_version_id, retry.trigger_evidence_kind, retry.trigger_evidence_ref_id, retry.accountable_user_id, retry.session_rollout_missing, retry.retired_session_id, retry.quick_actions_disabled, retry.regenerate_quick_actions_for, retry.branch_name, retry.durable_work_dir, retry.channel_context_revision, retry.comment_thread_id, retry.cancelled_by_type, retry.cancelled_by_id, retry.cancelled_by_name, retry.issue_snapshot, retry.workflow_step, retry.runtime_connector_handles
|
||
`
|
||
|
||
type FailExpiredRuntimeReconnectRetriesParams struct {
|
||
ReconnectGraceSecs float64 `json:"reconnect_grace_secs"`
|
||
RuntimeStaleSecs float64 `json:"runtime_stale_secs"`
|
||
MaxPerTick int32 `json:"max_per_tick"`
|
||
}
|
||
|
||
// A runtime_offline retry waits in deferred until its runtime returns healthy.
|
||
// Give that recovery state a bounded exit: after one full reconnect grace,
|
||
// fail it with a non-retryable reason so issues, agents, and runtime GC can
|
||
// converge. A runtime that is healthy when this query runs wins the race and
|
||
// remains deferred for the next daemon poll to promote. The parent join is the
|
||
// lineage discriminator; provider backoff and other deferred task types are
|
||
// intentionally excluded.
|
||
func (q *Queries) FailExpiredRuntimeReconnectRetries(ctx context.Context, arg FailExpiredRuntimeReconnectRetriesParams) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, failExpiredRuntimeReconnectRetries, arg.ReconnectGraceSecs, arg.RuntimeStaleSecs, arg.MaxPerTick)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const failStaleTasks = `-- name: FailStaleTasks :many
|
||
UPDATE agent_task_queue
|
||
SET status = 'failed', completed_at = now(), error = 'task timed out',
|
||
failure_reason = 'timeout',
|
||
prepare_lease_expires_at = NULL
|
||
WHERE (
|
||
status = 'dispatched'
|
||
AND dispatched_at < now() - make_interval(secs => $1::double precision)
|
||
AND (prepare_lease_expires_at IS NULL OR prepare_lease_expires_at < now())
|
||
AND (
|
||
runtime_id IS NULL
|
||
OR NOT EXISTS (
|
||
SELECT 1 FROM agent_runtime r
|
||
WHERE r.id = agent_task_queue.runtime_id
|
||
)
|
||
OR EXISTS (
|
||
SELECT 1 FROM agent_runtime r
|
||
WHERE r.id = agent_task_queue.runtime_id
|
||
AND (
|
||
(
|
||
r.status = 'online'
|
||
AND COALESCE(r.last_seen_at, r.updated_at) >=
|
||
now() - make_interval(secs => $2::double precision)
|
||
)
|
||
OR COALESCE(r.last_seen_at, r.updated_at) <
|
||
now() - make_interval(secs => $3::double precision)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
OR (
|
||
status = 'running'
|
||
AND started_at < now() - make_interval(secs => $4::double precision)
|
||
AND (
|
||
runtime_id IS NULL
|
||
OR NOT EXISTS (
|
||
SELECT 1 FROM agent_runtime r
|
||
WHERE r.id = agent_task_queue.runtime_id
|
||
AND COALESCE(r.last_seen_at, r.updated_at) >=
|
||
now() - make_interval(secs => $3::double precision)
|
||
)
|
||
)
|
||
)
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type FailStaleTasksParams struct {
|
||
DispatchTimeoutSecs float64 `json:"dispatch_timeout_secs"`
|
||
RuntimeStaleSecs float64 `json:"runtime_stale_secs"`
|
||
RuntimeReconnectGraceSecs float64 `json:"runtime_reconnect_grace_secs"`
|
||
RunningTimeoutSecs float64 `json:"running_timeout_secs"`
|
||
}
|
||
|
||
// Fails tasks stuck in dispatched/running beyond the given thresholds.
|
||
//
|
||
// Each branch pairs a wall-clock deadline with a task-appropriate liveness
|
||
// signal, so the sweeper only kills tasks whose owning daemon is no longer
|
||
// proving it is alive:
|
||
//
|
||
// - Dispatched: `prepare_lease_expires_at` is refreshed every 15s by the
|
||
// daemon between claim and StartTask (see startTaskPrepareLeaseExtender).
|
||
// A live lease excludes the row. An expired lease may fail immediately on
|
||
// a healthy runtime, but a disconnected runtime gets the same reconnect
|
||
// grace as a running task.
|
||
//
|
||
// - Running: no per-task lease is renewed once StartTask fires, so we key
|
||
// off the daemon-wide heartbeat instead — `agent_runtime.last_seen_at`,
|
||
// which the daemon bumps every ~15s while it is up. A running task is not
|
||
// killed until that heartbeat has been absent for the full reconnect
|
||
// grace, even when its own wall-clock timeout elapsed earlier. This keeps
|
||
// healthy multi-hour work alive through a network partition.
|
||
//
|
||
// The daemon-dead case is recovered immediately when that daemon restarts via
|
||
// RecoverOrphanedTasksForRuntime. Until then, this query and
|
||
// FailTasksForOfflineRuntimes share the same bounded reconnect grace.
|
||
//
|
||
// runtime_id IS NULL: a running row with no runtime is by definition not
|
||
// proving liveness, so the wall clock is allowed to fire — same shape as
|
||
// the legacy pure-wall-clock behavior for that (rare / historical) case.
|
||
//
|
||
// waiting_local_directory rows are intentionally excluded: the daemon owns
|
||
// the wait (with its own ctx-driven timeout) and a legitimate queue ahead
|
||
// of this task can exceed the dispatch / running timeouts without being
|
||
// "stuck". If the daemon dies, RecoverOrphanedTasksForRuntime reclaims
|
||
// those rows at restart.
|
||
func (q *Queries) FailStaleTasks(ctx context.Context, arg FailStaleTasksParams) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, failStaleTasks,
|
||
arg.DispatchTimeoutSecs,
|
||
arg.RuntimeStaleSecs,
|
||
arg.RuntimeReconnectGraceSecs,
|
||
arg.RunningTimeoutSecs,
|
||
)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const getAgent = `-- name: GetAgent :one
|
||
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist FROM agent
|
||
WHERE id = $1
|
||
`
|
||
|
||
func (q *Queries) GetAgent(ctx context.Context, id pgtype.UUID) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, getAgent, id)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const getAgentBySystemKey = `-- name: GetAgentBySystemKey :one
|
||
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist FROM agent
|
||
WHERE workspace_id = $1 AND system_key = $2 AND archived_at IS NULL
|
||
ORDER BY created_at ASC, id ASC
|
||
LIMIT 1
|
||
`
|
||
|
||
type GetAgentBySystemKeyParams struct {
|
||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||
SystemKey pgtype.Text `json:"system_key"`
|
||
}
|
||
|
||
// Resolves a workspace's built-in agent by its stable system_key. This is the
|
||
// identity lookup for system agents: their display name is owner-editable, so
|
||
// nothing server-side may key off it.
|
||
func (q *Queries) GetAgentBySystemKey(ctx context.Context, arg GetAgentBySystemKeyParams) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, getAgentBySystemKey, arg.WorkspaceID, arg.SystemKey)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const getAgentForClaimUpdate = `-- name: GetAgentForClaimUpdate :one
|
||
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist FROM agent
|
||
WHERE id = $1
|
||
FOR UPDATE
|
||
`
|
||
|
||
func (q *Queries) GetAgentForClaimUpdate(ctx context.Context, id pgtype.UUID) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, getAgentForClaimUpdate, id)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const getAgentForUpdate = `-- name: GetAgentForUpdate :one
|
||
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist FROM agent
|
||
WHERE id = $1
|
||
FOR UPDATE
|
||
`
|
||
|
||
// Serializes read-modify-write updates to disabled_runtime_skills so two
|
||
// concurrent per-skill toggles cannot overwrite each other.
|
||
func (q *Queries) GetAgentForUpdate(ctx context.Context, id pgtype.UUID) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, getAgentForUpdate, id)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const getAgentInWorkspace = `-- name: GetAgentInWorkspace :one
|
||
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist FROM agent
|
||
WHERE id = $1 AND workspace_id = $2 AND kind = 'user'
|
||
`
|
||
|
||
type GetAgentInWorkspaceParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||
}
|
||
|
||
func (q *Queries) GetAgentInWorkspace(ctx context.Context, arg GetAgentInWorkspaceParams) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, getAgentInWorkspace, arg.ID, arg.WorkspaceID)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const getAgentTask = `-- name: GetAgentTask :one
|
||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles FROM agent_task_queue
|
||
WHERE id = $1
|
||
`
|
||
|
||
func (q *Queries) GetAgentTask(ctx context.Context, id pgtype.UUID) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, getAgentTask, id)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const getAgentTaskForDelegatedFailureUpdate = `-- name: GetAgentTaskForDelegatedFailureUpdate :one
|
||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles FROM agent_task_queue
|
||
WHERE id = $1
|
||
FOR UPDATE
|
||
`
|
||
|
||
// Serializes the idempotent delegated-failure recovery signal for one failed
|
||
// task. FailTask and the stale-task sweepers can converge on the same row; the
|
||
// lock ensures they cannot create two recovery comments/tasks for it.
|
||
func (q *Queries) GetAgentTaskForDelegatedFailureUpdate(ctx context.Context, id pgtype.UUID) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, getAgentTaskForDelegatedFailureUpdate, id)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const getAgentTaskInWorkspace = `-- name: GetAgentTaskInWorkspace :one
|
||
SELECT atq.id, atq.agent_id, atq.issue_id, atq.status, atq.priority, atq.dispatched_at, atq.started_at, atq.completed_at, atq.result, atq.error, atq.created_at, atq.context, atq.runtime_id, atq.session_id, atq.work_dir, atq.trigger_comment_id, atq.chat_session_id, atq.autopilot_run_id, atq.attempt, atq.max_attempts, atq.parent_task_id, atq.failure_reason, atq.trigger_summary, atq.force_fresh_session, atq.is_leader_task, atq.wait_reason, atq.initiator_user_id, atq.handoff_note, atq.prepare_lease_expires_at, atq.squad_id, atq.runtime_mcp_overlay, atq.escalation_for_task_id, atq.fire_at, atq.originator_user_id, atq.runtime_connected_apps, atq.coalesced_comment_ids, atq.delivered_comment_ids, atq.chat_input_task_id, atq.chat_finalize_deferred_at, atq.originator_source, atq.delegated_from_task_id, atq.retry_of_task_id, atq.rerun_of_task_id, atq.rule_version_id, atq.trigger_evidence_kind, atq.trigger_evidence_ref_id, atq.accountable_user_id, atq.session_rollout_missing, atq.retired_session_id, atq.quick_actions_disabled, atq.regenerate_quick_actions_for, atq.branch_name, atq.durable_work_dir, atq.channel_context_revision, atq.comment_thread_id, atq.cancelled_by_type, atq.cancelled_by_id, atq.cancelled_by_name, atq.issue_snapshot, atq.workflow_step, atq.runtime_connector_handles FROM agent_task_queue atq
|
||
JOIN agent a ON a.id = atq.agent_id
|
||
WHERE atq.id = $1 AND a.workspace_id = $2
|
||
`
|
||
|
||
type GetAgentTaskInWorkspaceParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||
}
|
||
|
||
// Loads a task only when its owning agent lives in the given workspace.
|
||
// agent_id is NOT NULL on every task row (and ON DELETE CASCADE, so the agent
|
||
// always exists), which makes this the universal tenant guard for
|
||
// user-initiated cancellation — independent of which optional source FK
|
||
// (issue / chat_session / autopilot_run) happens to be set. It is what lets
|
||
// run_only autopilot tasks and quick_create tasks (whose issue does not exist
|
||
// yet) be cancelled at all, instead of 404-ing on a missing source FK.
|
||
func (q *Queries) GetAgentTaskInWorkspace(ctx context.Context, arg GetAgentTaskInWorkspaceParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, getAgentTaskInWorkspace, arg.ID, arg.WorkspaceID)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const getAgentTaskStatus = `-- name: GetAgentTaskStatus :one
|
||
SELECT atq.status, a.workspace_id
|
||
FROM agent_task_queue atq
|
||
JOIN agent a ON a.id = atq.agent_id
|
||
WHERE atq.id = $1
|
||
`
|
||
|
||
type GetAgentTaskStatusRow struct {
|
||
Status string `json:"status"`
|
||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||
}
|
||
|
||
// Hot-path status polling needs only the task status and the owning agent's
|
||
// workspace for authorization. Keep this independent of optional source links
|
||
// (issue, chat session, autopilot run) so it needs no source-entity lookup.
|
||
func (q *Queries) GetAgentTaskStatus(ctx context.Context, id pgtype.UUID) (GetAgentTaskStatusRow, error) {
|
||
row := q.db.QueryRow(ctx, getAgentTaskStatus, id)
|
||
var i GetAgentTaskStatusRow
|
||
err := row.Scan(&i.Status, &i.WorkspaceID)
|
||
return i, err
|
||
}
|
||
|
||
const getCommentThreadRootID = `-- name: GetCommentThreadRootID :one
|
||
SELECT comment_thread_root_id($1::uuid)::uuid AS id
|
||
`
|
||
|
||
func (q *Queries) GetCommentThreadRootID(ctx context.Context, commentID pgtype.UUID) (pgtype.UUID, error) {
|
||
row := q.db.QueryRow(ctx, getCommentThreadRootID, commentID)
|
||
var id pgtype.UUID
|
||
err := row.Scan(&id)
|
||
return id, err
|
||
}
|
||
|
||
const getLastTaskSession = `-- name: GetLastTaskSession :one
|
||
WITH retired_sessions AS (
|
||
SELECT DISTINCT r.retired_session_id AS session_id
|
||
FROM agent_task_queue r
|
||
WHERE r.agent_id = $1 AND r.issue_id = $2
|
||
AND COALESCE(r.context->>'type', '') <> 'triage'
|
||
AND r.retired_session_id IS NOT NULL
|
||
), resume_overflow_at AS (
|
||
SELECT MAX(COALESCE(t.completed_at, t.started_at, t.dispatched_at, t.created_at)) AS at
|
||
FROM agent_task_queue t
|
||
WHERE t.agent_id = $1 AND t.issue_id = $2
|
||
AND COALESCE(t.context->>'type', '') <> 'triage'
|
||
AND t.status = 'failed'
|
||
AND (
|
||
COALESCE(t.failure_reason, '') = 'codex_resume_oversized'
|
||
OR (COALESCE(t.error, '') ILIKE '%thread/resume failed%' AND COALESCE(t.error, '') ILIKE '%token too long%')
|
||
)
|
||
), latest_per_session AS (
|
||
SELECT DISTINCT ON (t.session_id)
|
||
t.session_id, t.work_dir, t.runtime_id, t.status, t.failure_reason, t.error,
|
||
t.started_at, t.issue_snapshot,
|
||
COALESCE(t.completed_at, t.started_at, t.dispatched_at, t.created_at) AS terminal_at
|
||
FROM agent_task_queue t
|
||
WHERE t.agent_id = $1 AND t.issue_id = $2
|
||
AND COALESCE(t.context->>'type', '') <> 'triage'
|
||
AND t.session_id IS NOT NULL
|
||
AND t.status IN ('completed', 'failed', 'cancelled')
|
||
ORDER BY t.session_id, COALESCE(t.completed_at, t.started_at, t.dispatched_at, t.created_at) DESC
|
||
)
|
||
SELECT session_id, work_dir, runtime_id, status, started_at, issue_snapshot FROM latest_per_session
|
||
WHERE session_id NOT IN (SELECT session_id FROM retired_sessions)
|
||
AND (
|
||
status IN ('completed', 'cancelled')
|
||
OR (
|
||
status = 'failed'
|
||
AND COALESCE(failure_reason, '') NOT IN ('iteration_limit', 'agent_fallback_message', 'api_invalid_request', 'codex_semantic_inactivity', 'agent_error.context_overflow', 'codex_resume_oversized')
|
||
AND NOT (COALESCE(error, '') ILIKE '%400%' AND COALESCE(error, '') ILIKE '%invalid_request_error%')
|
||
AND NOT (COALESCE(error, '') ILIKE '%image dimensions exceed max allowed size%' AND COALESCE(error, '') ILIKE '%image.source.base64.data%')
|
||
-- A provider credential-resolution failure ("Could not resolve
|
||
-- authentication method...") is deterministic on resume: the missing
|
||
-- api_key / auth_token / header is baked into the session's provider
|
||
-- state, so replaying it reproduces the same provider error forever. It
|
||
-- is classified agent_error.unknown (resume-safe), so this text guard is
|
||
-- the only thing that keeps a wedged issue from resuming the same dead
|
||
-- session on its next trigger — there is no daemon upgrade to wait for.
|
||
-- Keep in sync with ResumeUnsafeFailure and GetLastChatTaskSession.
|
||
-- The phrase itself lives in taskfailure.AuthMethodUnresolved, which the
|
||
-- daemon's in-turn fresh-session retry reads (GH #6777). This guard stays
|
||
-- because it is the only protection for rows an older daemon wrote.
|
||
AND NOT (COALESCE(error, '') ILIKE '%could not resolve authentication method%')
|
||
AND NOT (COALESCE(error, '') ~* 'must not be empty|must be non-?empty|must have non-?empty|non-?empty content|cannot be empty|should not be empty'
|
||
AND COALESCE(error, '') ~* 'role[^a-z0-9]{0,2}assistant|assistant message|message at position|messages\.[0-9]|messages\[[0-9]')
|
||
)
|
||
)
|
||
-- MUL-5722: a resume that overflowed the reader names no session, so it can
|
||
-- only be excluded by time, not by matching the failed row. Drop every
|
||
-- session whose last terminal activity predates the newest such failure: one
|
||
-- of them IS the oversized thread, and the row that would tell us which is
|
||
-- exactly the row that could not be written. Anything that terminated after
|
||
-- the overflow is the fresh thread that replaced it, so this un-blocks itself
|
||
-- as soon as one succeeds.
|
||
AND (
|
||
(SELECT at FROM resume_overflow_at) IS NULL
|
||
OR terminal_at > (SELECT at FROM resume_overflow_at)
|
||
)
|
||
ORDER BY terminal_at DESC
|
||
LIMIT 1
|
||
`
|
||
|
||
type GetLastTaskSessionParams struct {
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
}
|
||
|
||
type GetLastTaskSessionRow struct {
|
||
SessionID pgtype.Text `json:"session_id"`
|
||
WorkDir pgtype.Text `json:"work_dir"`
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
Status string `json:"status"`
|
||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||
IssueSnapshot []byte `json:"issue_snapshot"`
|
||
}
|
||
|
||
// Find resume context for an agent/issue from completed, failed, or cancelled
|
||
// execution tasks. Manual rerun uses its exact source task instead; its fresh
|
||
// flag also protects older claim handlers during mixed-version deployment.
|
||
//
|
||
// Reduce to the newest terminal row per session before applying safety filters;
|
||
// otherwise an older healthy row could revive the same poisoned session.
|
||
// Exclude explicitly retired sessions even when clean older rows still name them.
|
||
// Triage sessions must never cross into execution or vice versa.
|
||
//
|
||
// Keep failure-reason and text guards aligned with GetLastChatTaskSession and
|
||
// resumeUnsafeFailureReason. Text guards protect legacy or unclassified rows.
|
||
// Resume overflow can leave no session ID, so its timestamp excludes earlier
|
||
// sessions until a later healthy terminal row restores eligibility.
|
||
// status, started_at and issue_snapshot ride along because the row this query
|
||
// picks IS the run whose context the next turn continues, and both of a claim's
|
||
// deltas must be measured from THAT run rather than from whichever run started
|
||
// last (MUL-7344). status is what says the run actually delivered its prompt to
|
||
// the provider: this query deliberately accepts failed and cancelled rows so
|
||
// their SESSION stays resumable, but such a row may have died before the agent
|
||
// ever ran, and its snapshot would then describe an issue the session never
|
||
// saw. The two are not always the same row: this query skips poisoned
|
||
// and retired sessions, so it can legitimately return an OLDER run than the
|
||
// newest one. Measuring against the newest one would then tell an agent whose
|
||
// resumed memory predates an edit that the issue is unchanged.
|
||
func (q *Queries) GetLastTaskSession(ctx context.Context, arg GetLastTaskSessionParams) (GetLastTaskSessionRow, error) {
|
||
row := q.db.QueryRow(ctx, getLastTaskSession, arg.AgentID, arg.IssueID)
|
||
var i GetLastTaskSessionRow
|
||
err := row.Scan(
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.RuntimeID,
|
||
&i.Status,
|
||
&i.StartedAt,
|
||
&i.IssueSnapshot,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const getLatestChatTaskRolloutMissing = `-- name: GetLatestChatTaskRolloutMissing :one
|
||
SELECT COALESCE(session_rollout_missing, FALSE) FROM agent_task_queue
|
||
WHERE chat_session_id = $1
|
||
AND (
|
||
$2::bigint IS NULL
|
||
OR COALESCE(channel_context_revision, 1) = $2::bigint
|
||
)
|
||
AND status IN ('completed', 'failed')
|
||
AND started_at IS NOT NULL
|
||
ORDER BY COALESCE(completed_at, started_at, dispatched_at, created_at) DESC
|
||
LIMIT 1
|
||
`
|
||
|
||
type GetLatestChatTaskRolloutMissingParams struct {
|
||
ChatSessionID pgtype.UUID `json:"chat_session_id"`
|
||
ChannelContextRevision pgtype.Int8 `json:"channel_context_revision"`
|
||
}
|
||
|
||
// Chat-session counterpart of GetLatestTaskRolloutMissing (MUL-5305): reports
|
||
// whether the most recent terminal task on this chat session withheld its Codex
|
||
// session because the rollout was missing. When true the next chat claim resumed
|
||
// an older session (or none), so it must disclose the continuity gap.
|
||
func (q *Queries) GetLatestChatTaskRolloutMissing(ctx context.Context, arg GetLatestChatTaskRolloutMissingParams) (bool, error) {
|
||
row := q.db.QueryRow(ctx, getLatestChatTaskRolloutMissing, arg.ChatSessionID, arg.ChannelContextRevision)
|
||
var session_rollout_missing bool
|
||
err := row.Scan(&session_rollout_missing)
|
||
return session_rollout_missing, err
|
||
}
|
||
|
||
const getLatestTaskRoleForIssueAndAgent = `-- name: GetLatestTaskRoleForIssueAndAgent :one
|
||
SELECT is_leader_task, squad_id FROM agent_task_queue
|
||
WHERE issue_id = $1 AND agent_id = $2
|
||
ORDER BY created_at DESC
|
||
LIMIT 1
|
||
`
|
||
|
||
type GetLatestTaskRoleForIssueAndAgentParams struct {
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
}
|
||
|
||
type GetLatestTaskRoleForIssueAndAgentRow struct {
|
||
IsLeaderTask bool `json:"is_leader_task"`
|
||
SquadID pgtype.UUID `json:"squad_id"`
|
||
}
|
||
|
||
// Returns the role markers from the agent's most recent task on this issue.
|
||
// Used by the squad-leader self-trigger guard to tell apart leader tasks,
|
||
// same-squad worker tasks, and generic agent tasks such as direct mentions or
|
||
// thread-parent replies.
|
||
func (q *Queries) GetLatestTaskRoleForIssueAndAgent(ctx context.Context, arg GetLatestTaskRoleForIssueAndAgentParams) (GetLatestTaskRoleForIssueAndAgentRow, error) {
|
||
row := q.db.QueryRow(ctx, getLatestTaskRoleForIssueAndAgent, arg.IssueID, arg.AgentID)
|
||
var i GetLatestTaskRoleForIssueAndAgentRow
|
||
err := row.Scan(&i.IsLeaderTask, &i.SquadID)
|
||
return i, err
|
||
}
|
||
|
||
const getLatestTaskRolloutMissing = `-- name: GetLatestTaskRolloutMissing :one
|
||
SELECT COALESCE(session_rollout_missing, FALSE) FROM agent_task_queue
|
||
WHERE agent_id = $1 AND issue_id = $2
|
||
AND COALESCE(context->>'type', '') <> 'triage'
|
||
AND status IN ('completed', 'failed')
|
||
AND started_at IS NOT NULL
|
||
ORDER BY COALESCE(completed_at, started_at, dispatched_at, created_at) DESC
|
||
LIMIT 1
|
||
`
|
||
|
||
type GetLatestTaskRolloutMissingParams struct {
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
}
|
||
|
||
// Reports whether the most recent terminal task for (agent_id, issue_id)
|
||
// withheld its Codex session because the rollout was missing (MUL-5305). When
|
||
// true, GetLastTaskSession fell back to an older session, so the next run must
|
||
// disclose that the most recent turn's context could not be carried over. Any
|
||
// later task that records a real session resets this to FALSE by being the new
|
||
// most-recent row, so the disclosure fires once and then clears.
|
||
//
|
||
// Triage runs are excluded for the same reason GetLastTaskSession excludes them
|
||
// (MUL-7189 §5.6), and the two must agree: this query only reports whether THAT
|
||
// one fell back. A triage run is invisible to it, so a triage run reported here
|
||
// would tell the first execution run after accept that the previous turn's
|
||
// context could not be carried over — about a turn it was never entitled to.
|
||
func (q *Queries) GetLatestTaskRolloutMissing(ctx context.Context, arg GetLatestTaskRolloutMissingParams) (bool, error) {
|
||
row := q.db.QueryRow(ctx, getLatestTaskRolloutMissing, arg.AgentID, arg.IssueID)
|
||
var session_rollout_missing bool
|
||
err := row.Scan(&session_rollout_missing)
|
||
return session_rollout_missing, err
|
||
}
|
||
|
||
const getWorkspaceAgentActivity30d = `-- name: GetWorkspaceAgentActivity30d :many
|
||
SELECT
|
||
atq.agent_id,
|
||
DATE_TRUNC('day', atq.completed_at)::timestamptz AS bucket,
|
||
COUNT(*)::int AS task_count,
|
||
COUNT(*) FILTER (WHERE atq.status = 'failed')::int AS failed_count,
|
||
COUNT(*) FILTER (WHERE atq.status = 'completed')::int AS completed_count,
|
||
COUNT(*) FILTER (WHERE atq.status = 'cancelled')::int AS cancelled_count,
|
||
COALESCE(SUM(EXTRACT(EPOCH FROM (atq.completed_at - atq.started_at)) * 1000)
|
||
FILTER (WHERE atq.completed_at > atq.started_at), 0)::float8 AS duration_ms,
|
||
COUNT(*) FILTER (WHERE atq.completed_at > atq.started_at)::int AS duration_count
|
||
FROM agent_task_queue atq
|
||
JOIN agent a ON a.id = atq.agent_id
|
||
WHERE a.workspace_id = $1
|
||
AND atq.completed_at IS NOT NULL
|
||
AND atq.completed_at > now() - INTERVAL '30 days'
|
||
GROUP BY atq.agent_id, bucket
|
||
ORDER BY atq.agent_id, bucket
|
||
`
|
||
|
||
type GetWorkspaceAgentActivity30dRow struct {
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
Bucket pgtype.Timestamptz `json:"bucket"`
|
||
TaskCount int32 `json:"task_count"`
|
||
FailedCount int32 `json:"failed_count"`
|
||
CompletedCount int32 `json:"completed_count"`
|
||
CancelledCount int32 `json:"cancelled_count"`
|
||
DurationMs float64 `json:"duration_ms"`
|
||
DurationCount int32 `json:"duration_count"`
|
||
}
|
||
|
||
// Returns per-agent daily activity buckets for the last 30 days. Single
|
||
// workspace-wide read backs both surfaces:
|
||
// - Agents list ACTIVITY column — uses only the trailing 7 buckets
|
||
// - Agent detail "Last 30 days" panel — uses the full 30
|
||
//
|
||
// 30 days contains 7 days, so one fetch + a client-side .slice(-7) wins
|
||
// over fetching twice. Days with no completion produce no row; the
|
||
// front-end zero-fills.
|
||
//
|
||
// Anchored on completed_at (not created_at) because the sparkline answers
|
||
// "what did this agent produce?" not "what was queued at it?". A task that's
|
||
// still in flight has no completed_at and contributes nothing here — that's
|
||
// correct: in-flight tasks are surfaced via the live presence indicator,
|
||
// not the historical trend.
|
||
// Keep total activity separate from outcomes: cancelled runs belong in the
|
||
// history, but success rate is completed / (completed + failed).
|
||
func (q *Queries) GetWorkspaceAgentActivity30d(ctx context.Context, workspaceID pgtype.UUID) ([]GetWorkspaceAgentActivity30dRow, error) {
|
||
rows, err := q.db.Query(ctx, getWorkspaceAgentActivity30d, workspaceID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []GetWorkspaceAgentActivity30dRow{}
|
||
for rows.Next() {
|
||
var i GetWorkspaceAgentActivity30dRow
|
||
if err := rows.Scan(
|
||
&i.AgentID,
|
||
&i.Bucket,
|
||
&i.TaskCount,
|
||
&i.FailedCount,
|
||
&i.CompletedCount,
|
||
&i.CancelledCount,
|
||
&i.DurationMs,
|
||
&i.DurationCount,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const getWorkspaceAgentRunCounts = `-- name: GetWorkspaceAgentRunCounts :many
|
||
SELECT
|
||
atq.agent_id,
|
||
COUNT(*)::int AS run_count
|
||
FROM agent_task_queue atq
|
||
JOIN agent a ON a.id = atq.agent_id
|
||
WHERE a.workspace_id = $1
|
||
AND atq.created_at > now() - INTERVAL '30 days'
|
||
GROUP BY atq.agent_id
|
||
`
|
||
|
||
type GetWorkspaceAgentRunCountsRow struct {
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
RunCount int32 `json:"run_count"`
|
||
}
|
||
|
||
// Total task runs per agent over the trailing 30 days, used by the Agents
|
||
// list RUNS column. 30-day window keeps the count meaningful (a long-dormant
|
||
// agent shouldn't show "5,420 runs from 2 years ago") and keeps the scan
|
||
// bounded as the workspace ages.
|
||
func (q *Queries) GetWorkspaceAgentRunCounts(ctx context.Context, workspaceID pgtype.UUID) ([]GetWorkspaceAgentRunCountsRow, error) {
|
||
rows, err := q.db.Query(ctx, getWorkspaceAgentRunCounts, workspaceID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []GetWorkspaceAgentRunCountsRow{}
|
||
for rows.Next() {
|
||
var i GetWorkspaceAgentRunCountsRow
|
||
if err := rows.Scan(&i.AgentID, &i.RunCount); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const hasActiveTaskForIssue = `-- name: HasActiveTaskForIssue :one
|
||
SELECT count(*) > 0 AS has_active FROM agent_task_queue
|
||
WHERE issue_id = $1 AND status IN ('queued', 'dispatched', 'running', 'waiting_local_directory')
|
||
`
|
||
|
||
// Returns true if there is any queued, dispatched, waiting_local_directory,
|
||
// or running task for the issue.
|
||
func (q *Queries) HasActiveTaskForIssue(ctx context.Context, issueID pgtype.UUID) (bool, error) {
|
||
row := q.db.QueryRow(ctx, hasActiveTaskForIssue, issueID)
|
||
var has_active bool
|
||
err := row.Scan(&has_active)
|
||
return has_active, err
|
||
}
|
||
|
||
const hasActiveTaskForIssueAndAgent = `-- name: HasActiveTaskForIssueAndAgent :one
|
||
SELECT count(*) > 0 AS has_active FROM agent_task_queue
|
||
WHERE issue_id = $1 AND agent_id = $2
|
||
AND (
|
||
status IN ('queued', 'dispatched', 'running', 'waiting_local_directory')
|
||
OR (status = 'deferred' AND context->>'channel_issue_media_pending' = 'true')
|
||
)
|
||
`
|
||
|
||
type HasActiveTaskForIssueAndAgentParams struct {
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
}
|
||
|
||
// MUL-4195: true when the (issue, agent) pair has any non-terminal task in a
|
||
// state whose completion will run completion reconciliation — queued,
|
||
// dispatched, running, waiting_local_directory, or the explicitly-marked
|
||
// channel-media deferred state. Used by the comment enqueue
|
||
// path: when a merge into a pre-claim task fails (the task is already
|
||
// dispatched/running, or a mismatched pre-claim task exists), a fresh queued
|
||
// INSERT would collide with idx_one_pending_task_per_issue_agent AND would risk
|
||
// a duplicate run. Instead the caller relies on that active task's completion
|
||
// reconcile to schedule the guaranteed follow-up, and only enqueues fresh when
|
||
// NO active task exists.
|
||
func (q *Queries) HasActiveTaskForIssueAndAgent(ctx context.Context, arg HasActiveTaskForIssueAndAgentParams) (bool, error) {
|
||
row := q.db.QueryRow(ctx, hasActiveTaskForIssueAndAgent, arg.IssueID, arg.AgentID)
|
||
var has_active bool
|
||
err := row.Scan(&has_active)
|
||
return has_active, err
|
||
}
|
||
|
||
const hasActiveTaskForIssueAndAgentInThread = `-- name: HasActiveTaskForIssueAndAgentInThread :one
|
||
SELECT count(*) > 0 AS has_active FROM agent_task_queue
|
||
WHERE issue_id = $1 AND agent_id = $2
|
||
AND (
|
||
status IN ('queued', 'dispatched', 'running', 'waiting_local_directory')
|
||
OR (status = 'deferred' AND context->>'channel_issue_media_pending' = 'true')
|
||
)
|
||
AND comment_thread_id IS NOT DISTINCT FROM comment_thread_root_id($3::uuid)
|
||
`
|
||
|
||
type HasActiveTaskForIssueAndAgentInThreadParams struct {
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
ThreadCommentID pgtype.UUID `json:"thread_comment_id"`
|
||
}
|
||
|
||
// Active execution and pending work in this comment thread only.
|
||
func (q *Queries) HasActiveTaskForIssueAndAgentInThread(ctx context.Context, arg HasActiveTaskForIssueAndAgentInThreadParams) (bool, error) {
|
||
row := q.db.QueryRow(ctx, hasActiveTaskForIssueAndAgentInThread, arg.IssueID, arg.AgentID, arg.ThreadCommentID)
|
||
var has_active bool
|
||
err := row.Scan(&has_active)
|
||
return has_active, err
|
||
}
|
||
|
||
const hasPendingTaskForIssue = `-- name: HasPendingTaskForIssue :one
|
||
SELECT count(*) > 0 AS has_pending FROM agent_task_queue
|
||
WHERE context->>'wakeup_id' IS NULL AND issue_id = $1 AND status IN ('queued', 'dispatched')
|
||
`
|
||
|
||
// Returns true if there is a queued or dispatched (but not yet running) task for the issue.
|
||
// Used by the coalescing queue: allow enqueue when a task is running (so
|
||
// the agent picks up new comments on the next cycle) but skip if a pending
|
||
// task already exists (natural dedup).
|
||
func (q *Queries) HasPendingTaskForIssue(ctx context.Context, issueID pgtype.UUID) (bool, error) {
|
||
row := q.db.QueryRow(ctx, hasPendingTaskForIssue, issueID)
|
||
var has_pending bool
|
||
err := row.Scan(&has_pending)
|
||
return has_pending, err
|
||
}
|
||
|
||
const hasPendingTaskForIssueAndAgent = `-- name: HasPendingTaskForIssueAndAgent :one
|
||
SELECT count(*) > 0 AS has_pending FROM agent_task_queue
|
||
WHERE context->>'wakeup_id' IS NULL AND issue_id = $1 AND agent_id = $2
|
||
AND (
|
||
status IN ('queued', 'dispatched')
|
||
OR (status = 'deferred' AND context->>'channel_issue_media_pending' = 'true')
|
||
)
|
||
AND (
|
||
COALESCE($3::text, '') = ''
|
||
OR context->>'head_sha' = $3::text
|
||
)
|
||
`
|
||
|
||
type HasPendingTaskForIssueAndAgentParams struct {
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
HeadSha pgtype.Text `json:"head_sha"`
|
||
}
|
||
|
||
// Returns true if a specific agent already has a queued or dispatched task
|
||
// for the given issue, or the explicitly-marked deferred task whose channel
|
||
// media must bind before the issue agent runs. Used by @mention trigger dedup.
|
||
//
|
||
// head_sha keys the dedup on the commit under review (TEN-356): when a caller
|
||
// passes a non-empty head_sha, a pending task only dedups if it was stamped
|
||
// with the SAME head_sha at enqueue time. If HEAD advanced since the pending
|
||
// task's run began (its context head_sha differs, or predates the stamp and is
|
||
// NULL), the dedup MISSES and a fresh review enqueues against the new HEAD.
|
||
// When head_sha is empty/NULL (issue has no linked PR) the check falls back to
|
||
// the pre-TEN-356 (issue_id, agent_id) key so non-PR issues keep coalescing.
|
||
func (q *Queries) HasPendingTaskForIssueAndAgent(ctx context.Context, arg HasPendingTaskForIssueAndAgentParams) (bool, error) {
|
||
row := q.db.QueryRow(ctx, hasPendingTaskForIssueAndAgent, arg.IssueID, arg.AgentID, arg.HeadSha)
|
||
var has_pending bool
|
||
err := row.Scan(&has_pending)
|
||
return has_pending, err
|
||
}
|
||
|
||
const hasPendingTaskForIssueAndAgentExcludingTriggerComment = `-- name: HasPendingTaskForIssueAndAgentExcludingTriggerComment :one
|
||
SELECT count(*) > 0 AS has_pending FROM agent_task_queue
|
||
WHERE context->>'wakeup_id' IS NULL AND issue_id = $1
|
||
AND agent_id = $2
|
||
AND (
|
||
status IN ('queued', 'dispatched')
|
||
OR (status = 'deferred' AND context->>'channel_issue_media_pending' = 'true')
|
||
)
|
||
AND trigger_comment_id IS DISTINCT FROM $3::uuid
|
||
AND (
|
||
COALESCE($4::text, '') = ''
|
||
OR context->>'head_sha' = $4::text
|
||
)
|
||
`
|
||
|
||
type HasPendingTaskForIssueAndAgentExcludingTriggerCommentParams struct {
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
ExcludeTriggerCommentID pgtype.UUID `json:"exclude_trigger_comment_id"`
|
||
HeadSha pgtype.Text `json:"head_sha"`
|
||
}
|
||
|
||
// Same as HasPendingTaskForIssueAndAgent, but ignores tasks triggered by the
|
||
// current comment being edited. Edit preview needs this because save cancels
|
||
// that comment's old queued/dispatched tasks before re-computing triggers.
|
||
// Carries the same head_sha dedup key as HasPendingTaskForIssueAndAgent (TEN-356).
|
||
func (q *Queries) HasPendingTaskForIssueAndAgentExcludingTriggerComment(ctx context.Context, arg HasPendingTaskForIssueAndAgentExcludingTriggerCommentParams) (bool, error) {
|
||
row := q.db.QueryRow(ctx, hasPendingTaskForIssueAndAgentExcludingTriggerComment,
|
||
arg.IssueID,
|
||
arg.AgentID,
|
||
arg.ExcludeTriggerCommentID,
|
||
arg.HeadSha,
|
||
)
|
||
var has_pending bool
|
||
err := row.Scan(&has_pending)
|
||
return has_pending, err
|
||
}
|
||
|
||
const hasPendingTaskForIssueAndAgentExcludingTriggerCommentInThread = `-- name: HasPendingTaskForIssueAndAgentExcludingTriggerCommentInThread :one
|
||
SELECT count(*) > 0 AS has_pending FROM agent_task_queue
|
||
WHERE context->>'wakeup_id' IS NULL AND issue_id = $1
|
||
AND agent_id = $2
|
||
AND (
|
||
status IN ('queued', 'dispatched')
|
||
OR (status = 'deferred' AND context->>'channel_issue_media_pending' = 'true')
|
||
)
|
||
AND trigger_comment_id IS DISTINCT FROM $3::uuid
|
||
AND (
|
||
COALESCE($4::text, '') = ''
|
||
OR context->>'head_sha' = $4::text
|
||
)
|
||
AND comment_thread_id IS NOT DISTINCT FROM comment_thread_root_id($5::uuid)
|
||
`
|
||
|
||
type HasPendingTaskForIssueAndAgentExcludingTriggerCommentInThreadParams struct {
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
ExcludeTriggerCommentID pgtype.UUID `json:"exclude_trigger_comment_id"`
|
||
HeadSha pgtype.Text `json:"head_sha"`
|
||
ThreadCommentID pgtype.UUID `json:"thread_comment_id"`
|
||
}
|
||
|
||
// Thread-scoped edit preview: ignore the comment whose old run save replaces.
|
||
func (q *Queries) HasPendingTaskForIssueAndAgentExcludingTriggerCommentInThread(ctx context.Context, arg HasPendingTaskForIssueAndAgentExcludingTriggerCommentInThreadParams) (bool, error) {
|
||
row := q.db.QueryRow(ctx, hasPendingTaskForIssueAndAgentExcludingTriggerCommentInThread,
|
||
arg.IssueID,
|
||
arg.AgentID,
|
||
arg.ExcludeTriggerCommentID,
|
||
arg.HeadSha,
|
||
arg.ThreadCommentID,
|
||
)
|
||
var has_pending bool
|
||
err := row.Scan(&has_pending)
|
||
return has_pending, err
|
||
}
|
||
|
||
const hasPendingTaskForIssueAndAgentInThread = `-- name: HasPendingTaskForIssueAndAgentInThread :one
|
||
SELECT count(*) > 0 AS has_pending FROM agent_task_queue
|
||
WHERE context->>'wakeup_id' IS NULL AND issue_id = $1 AND agent_id = $2
|
||
AND (
|
||
status IN ('queued', 'dispatched')
|
||
OR (status = 'deferred' AND context->>'channel_issue_media_pending' = 'true')
|
||
)
|
||
AND (
|
||
COALESCE($3::text, '') = ''
|
||
OR context->>'head_sha' = $3::text
|
||
)
|
||
AND comment_thread_id IS NOT DISTINCT FROM comment_thread_root_id($4::uuid)
|
||
`
|
||
|
||
type HasPendingTaskForIssueAndAgentInThreadParams struct {
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
HeadSha pgtype.Text `json:"head_sha"`
|
||
ThreadCommentID pgtype.UUID `json:"thread_comment_id"`
|
||
}
|
||
|
||
// Same pending/head rules as HasPendingTaskForIssueAndAgent, scoped to one
|
||
// root comment and all descendants. NULL selects assignment-level work.
|
||
func (q *Queries) HasPendingTaskForIssueAndAgentInThread(ctx context.Context, arg HasPendingTaskForIssueAndAgentInThreadParams) (bool, error) {
|
||
row := q.db.QueryRow(ctx, hasPendingTaskForIssueAndAgentInThread,
|
||
arg.IssueID,
|
||
arg.AgentID,
|
||
arg.HeadSha,
|
||
arg.ThreadCommentID,
|
||
)
|
||
var has_pending bool
|
||
err := row.Scan(&has_pending)
|
||
return has_pending, err
|
||
}
|
||
|
||
const hasRetryTaskForParent = `-- name: HasRetryTaskForParent :one
|
||
SELECT count(*) > 0
|
||
FROM agent_task_queue
|
||
WHERE parent_task_id = $1
|
||
AND status <> 'cancelled'
|
||
`
|
||
|
||
// Defense-in-depth for the recovery path: a delegated failure with any retry
|
||
// child is still intermediate and must not wake its coordinator.
|
||
func (q *Queries) HasRetryTaskForParent(ctx context.Context, parentTaskID pgtype.UUID) (bool, error) {
|
||
row := q.db.QueryRow(ctx, hasRetryTaskForParent, parentTaskID)
|
||
var column_1 bool
|
||
err := row.Scan(&column_1)
|
||
return column_1, err
|
||
}
|
||
|
||
const hasTaskCoveringDelegatedFailureComment = `-- name: HasTaskCoveringDelegatedFailureComment :one
|
||
SELECT count(*) > 0 AS covered
|
||
FROM agent_task_queue
|
||
WHERE issue_id = $1
|
||
AND agent_id = $2
|
||
AND (
|
||
$3::uuid = ANY(delivered_comment_ids)
|
||
OR (
|
||
id IS DISTINCT FROM $4::uuid
|
||
AND (
|
||
status IN ('queued', 'dispatched', 'running', 'waiting_local_directory')
|
||
OR (status = 'deferred' AND context->>'channel_issue_media_pending' = 'true')
|
||
)
|
||
AND (trigger_comment_id = $3::uuid OR $3::uuid = ANY(coalesced_comment_ids))
|
||
)
|
||
)
|
||
`
|
||
|
||
type HasTaskCoveringDelegatedFailureCommentParams struct {
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
CommentID pgtype.UUID `json:"comment_id"`
|
||
ExcludeTaskID pgtype.UUID `json:"exclude_task_id"`
|
||
}
|
||
|
||
// Durable idempotency check for a recovery comment. The completion reconciler
|
||
// excludes its own just-completed task when replaying a planned-but-undelivered
|
||
// signal. A planned comment is covered only while its task can still execute;
|
||
// after a task becomes terminal, delivered_comment_ids is the sole durable
|
||
// receipt. This prevents cancelled/failed pre-delivery tasks from swallowing
|
||
// the recovery obligation while still avoiding a loop after real delivery.
|
||
func (q *Queries) HasTaskCoveringDelegatedFailureComment(ctx context.Context, arg HasTaskCoveringDelegatedFailureCommentParams) (bool, error) {
|
||
row := q.db.QueryRow(ctx, hasTaskCoveringDelegatedFailureComment,
|
||
arg.IssueID,
|
||
arg.AgentID,
|
||
arg.CommentID,
|
||
arg.ExcludeTaskID,
|
||
)
|
||
var covered bool
|
||
err := row.Scan(&covered)
|
||
return covered, err
|
||
}
|
||
|
||
const hasTaskForIssue = `-- name: HasTaskForIssue :one
|
||
SELECT EXISTS (SELECT 1 FROM agent_task_queue WHERE issue_id = $1)
|
||
`
|
||
|
||
// Returns true if the issue has any task in any status. Webhook recovery
|
||
// treats even a terminal task as proof that ownership moved downstream.
|
||
func (q *Queries) HasTaskForIssue(ctx context.Context, issueID pgtype.UUID) (bool, error) {
|
||
row := q.db.QueryRow(ctx, hasTaskForIssue, issueID)
|
||
var exists bool
|
||
err := row.Scan(&exists)
|
||
return exists, err
|
||
}
|
||
|
||
const linkTaskToIssue = `-- name: LinkTaskToIssue :exec
|
||
UPDATE agent_task_queue
|
||
SET issue_id = $2
|
||
WHERE id = $1 AND issue_id IS NULL
|
||
AND lock_task_owner_rows(NULL, $2, NULL)
|
||
`
|
||
|
||
type LinkTaskToIssueParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
}
|
||
|
||
// Fenced against workspace teardown: lock_task_owner_rows (migration 284)
|
||
// locks the owners' workspace rows in the writer's own transaction and returns
|
||
// false once they are gone, so this statement writes no row instead of stranding
|
||
// a task in a workspace that has just been deleted (MUL-5999).
|
||
// Attaches the issue a quick-create task produced back to the task row, once
|
||
// the agent has finished and the issue exists. Guarded by `issue_id IS NULL`
|
||
// so this never overwrites an issue id that was set at task creation (only
|
||
// quick-create tasks land here unset). Fixes the activity row staying on
|
||
// "Creating issue" forever after completion.
|
||
func (q *Queries) LinkTaskToIssue(ctx context.Context, arg LinkTaskToIssueParams) error {
|
||
_, err := q.db.Exec(ctx, linkTaskToIssue, arg.ID, arg.IssueID)
|
||
return err
|
||
}
|
||
|
||
const listActiveAgentsByRuntime = `-- name: ListActiveAgentsByRuntime :many
|
||
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist FROM agent
|
||
WHERE runtime_id = $1 AND archived_at IS NULL AND kind = 'user'
|
||
ORDER BY name ASC
|
||
`
|
||
|
||
// Returns every non-archived agent bound to a runtime. Backs the cascade
|
||
// delete dialog: when DELETE /api/runtimes/:id refuses with
|
||
// runtime_has_active_agents, the response carries this list so the front-end
|
||
// can render exactly the agents that will be unbound if the user confirms,
|
||
// and so the cascade endpoint's expected_active_agent_ids check has a stable
|
||
// snapshot to compare against. Ordered by name for a deterministic display.
|
||
func (q *Queries) ListActiveAgentsByRuntime(ctx context.Context, runtimeID pgtype.UUID) ([]Agent, error) {
|
||
rows, err := q.db.Query(ctx, listActiveAgentsByRuntime, runtimeID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []Agent{}
|
||
for rows.Next() {
|
||
var i Agent
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listActiveAgentsByRuntimeForUpdate = `-- name: ListActiveAgentsByRuntimeForUpdate :many
|
||
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist FROM agent
|
||
WHERE runtime_id = $1 AND archived_at IS NULL AND kind = 'user'
|
||
ORDER BY name ASC
|
||
FOR UPDATE
|
||
`
|
||
|
||
// FOR UPDATE variant used inside the cascade-delete transaction. Locks
|
||
// each currently-active agent row so a concurrent archive/move of one
|
||
// of those rows blocks until our transaction commits. Pair with
|
||
// LockAgentRuntime, which holds the runtime row exclusively to also
|
||
// block FK-validated INSERTs / runtime_id updates that would otherwise
|
||
// add a new agent to the runtime mid-cascade. Together they guarantee
|
||
// that the set we compared against expected_active_agent_ids is stable.
|
||
func (q *Queries) ListActiveAgentsByRuntimeForUpdate(ctx context.Context, runtimeID pgtype.UUID) ([]Agent, error) {
|
||
rows, err := q.db.Query(ctx, listActiveAgentsByRuntimeForUpdate, runtimeID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []Agent{}
|
||
for rows.Next() {
|
||
var i Agent
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listActiveTasksByIssue = `-- name: ListActiveTasksByIssue :many
|
||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles FROM agent_task_queue
|
||
WHERE issue_id = $1 AND status IN ('queued', 'dispatched', 'running', 'waiting_local_directory')
|
||
ORDER BY created_at DESC
|
||
`
|
||
|
||
// Backs the issue-detail "agent live" banner. Includes 'queued' so the
|
||
// banner shows up the moment a task is enqueued — not only after a runtime
|
||
// claims it. The queued window can be long when the runtime is offline or
|
||
// busy on a prior task, and a silent UI during that window looks like the
|
||
// platform never received the trigger.
|
||
func (q *Queries) ListActiveTasksByIssue(ctx context.Context, issueID pgtype.UUID) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, listActiveTasksByIssue, issueID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listActiveTasksByIssueFamily = `-- name: ListActiveTasksByIssueFamily :many
|
||
SELECT
|
||
atq.id AS task_id,
|
||
atq.agent_id,
|
||
atq.issue_id,
|
||
atq.status,
|
||
atq.created_at,
|
||
atq.started_at,
|
||
w.issue_prefix,
|
||
i.number AS issue_number,
|
||
i.title AS issue_title
|
||
FROM agent_task_queue atq
|
||
JOIN issue i ON i.id = atq.issue_id
|
||
JOIN workspace w ON w.id = i.workspace_id
|
||
WHERE i.workspace_id = $1
|
||
AND (i.id = $2::uuid OR i.parent_issue_id = $2::uuid)
|
||
AND atq.status IN ('queued', 'dispatched', 'running', 'waiting_local_directory')
|
||
ORDER BY
|
||
CASE atq.status
|
||
WHEN 'running' THEN 0
|
||
WHEN 'dispatched' THEN 1
|
||
WHEN 'waiting_local_directory' THEN 2
|
||
ELSE 3
|
||
END,
|
||
atq.created_at DESC
|
||
LIMIT $3
|
||
`
|
||
|
||
type ListActiveTasksByIssueFamilyParams struct {
|
||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||
RootIssueID pgtype.UUID `json:"root_issue_id"`
|
||
RowLimit int32 `json:"row_limit"`
|
||
}
|
||
|
||
type ListActiveTasksByIssueFamilyRow struct {
|
||
TaskID pgtype.UUID `json:"task_id"`
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
Status string `json:"status"`
|
||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||
IssuePrefix string `json:"issue_prefix"`
|
||
IssueNumber int32 `json:"issue_number"`
|
||
IssueTitle string `json:"issue_title"`
|
||
}
|
||
|
||
// Cross-issue coordination read for parallel sub-issue work (#7768). Given a
|
||
// family root — the target issue's parent, or the target itself when it has
|
||
// none — return every in-flight task on the root and on all of its children,
|
||
// so a run can see who else is already working in the family before it starts
|
||
// overlapping work. Advisory only: nothing here gates, queues, or serialises
|
||
// anything.
|
||
//
|
||
// Same active set as ListActiveTasksByIssue, including 'queued': a queued
|
||
// sibling cannot answer you yet, but it is about to touch the same code, which
|
||
// is exactly what the caller is trying to find out. The status column tells the
|
||
// two apart.
|
||
//
|
||
// Issue identity is joined in because the caller renders runs from several
|
||
// issues in one list and cannot label a row from the task alone. agent_id is
|
||
// here for the same reason: this read spans agents, and which one is on a
|
||
// sibling is the answer, not a detail.
|
||
//
|
||
// Columns are named rather than embedded. This is the coordination question,
|
||
// not the execution log: result and context are JSONB blobs, and work_dir /
|
||
// trigger_summary / the attribution ids are all execution-log fields that a
|
||
// caller asking "who else is here?" never reads. Selecting them would make
|
||
// Postgres detoast and ship roughly 5x the bytes per row for nothing.
|
||
//
|
||
// Ordered running-first so the truncation the LIMIT may impose drops the least
|
||
// interesting rows, and bounded because a parent with hundreds of children must
|
||
// not turn one coordination read into an unbounded scan.
|
||
func (q *Queries) ListActiveTasksByIssueFamily(ctx context.Context, arg ListActiveTasksByIssueFamilyParams) ([]ListActiveTasksByIssueFamilyRow, error) {
|
||
rows, err := q.db.Query(ctx, listActiveTasksByIssueFamily, arg.WorkspaceID, arg.RootIssueID, arg.RowLimit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []ListActiveTasksByIssueFamilyRow{}
|
||
for rows.Next() {
|
||
var i ListActiveTasksByIssueFamilyRow
|
||
if err := rows.Scan(
|
||
&i.TaskID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.CreatedAt,
|
||
&i.StartedAt,
|
||
&i.IssuePrefix,
|
||
&i.IssueNumber,
|
||
&i.IssueTitle,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listAgentTasks = `-- name: ListAgentTasks :many
|
||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles FROM agent_task_queue
|
||
WHERE agent_id = $1
|
||
-- Apply visibility before LIMIT so hidden fallbacks cannot end a page early.
|
||
-- Keep this predicate in sync with handler.visibleTaskHistory.
|
||
AND NOT (escalation_for_task_id IS NOT NULL AND started_at IS NULL
|
||
AND status IN ('deferred', 'cancelled'))
|
||
AND (created_at, id) < ($2::timestamptz, $3::uuid)
|
||
ORDER BY created_at DESC, id DESC
|
||
LIMIT $4
|
||
`
|
||
|
||
type ListAgentTasksParams struct {
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
BeforeCreatedAt pgtype.Timestamptz `json:"before_created_at"`
|
||
BeforeID pgtype.UUID `json:"before_id"`
|
||
PageLimit int32 `json:"page_limit"`
|
||
}
|
||
|
||
func (q *Queries) ListAgentTasks(ctx context.Context, arg ListAgentTasksParams) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, listAgentTasks,
|
||
arg.AgentID,
|
||
arg.BeforeCreatedAt,
|
||
arg.BeforeID,
|
||
arg.PageLimit,
|
||
)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listAgents = `-- name: ListAgents :many
|
||
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist FROM agent
|
||
WHERE workspace_id = $1 AND archived_at IS NULL AND kind = 'user'
|
||
ORDER BY created_at ASC
|
||
`
|
||
|
||
func (q *Queries) ListAgents(ctx context.Context, workspaceID pgtype.UUID) ([]Agent, error) {
|
||
rows, err := q.db.Query(ctx, listAgents, workspaceID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []Agent{}
|
||
for rows.Next() {
|
||
var i Agent
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listAllAgents = `-- name: ListAllAgents :many
|
||
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist FROM agent
|
||
WHERE workspace_id = $1 AND kind = 'user'
|
||
ORDER BY created_at ASC
|
||
`
|
||
|
||
func (q *Queries) ListAllAgents(ctx context.Context, workspaceID pgtype.UUID) ([]Agent, error) {
|
||
rows, err := q.db.Query(ctx, listAllAgents, workspaceID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []Agent{}
|
||
for rows.Next() {
|
||
var i Agent
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listAllAgentsAnyKind = `-- name: ListAllAgentsAnyKind :many
|
||
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist FROM agent
|
||
WHERE workspace_id = $1
|
||
ORDER BY created_at ASC
|
||
`
|
||
|
||
// Every agent row in the workspace, INCLUDING `kind = 'system'` execution
|
||
// carriers (agent builder sessions) that ListAllAgents deliberately hides.
|
||
//
|
||
// Only for surfaces that aggregate over raw `agent_task_queue` / task_usage
|
||
// rows, which carry no kind filter of their own: a system carrier runs real
|
||
// tasks and books real usage, so a per-agent rollup returns it whether or not
|
||
// any list endpoint will ever name it. Those surfaces need the full population
|
||
// to decide what to hide (MUL-5409). Do NOT use this to build a user-facing
|
||
// agent list — `kind = 'system'` must stay out of every picker and assignee
|
||
// surface.
|
||
func (q *Queries) ListAllAgentsAnyKind(ctx context.Context, workspaceID pgtype.UUID) ([]Agent, error) {
|
||
rows, err := q.db.Query(ctx, listAllAgentsAnyKind, workspaceID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []Agent{}
|
||
for rows.Next() {
|
||
var i Agent
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listChatFinalizeDeferredExpired = `-- name: ListChatFinalizeDeferredExpired :many
|
||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles FROM agent_task_queue
|
||
WHERE chat_finalize_deferred_at IS NOT NULL
|
||
AND chat_finalize_deferred_at < now() - make_interval(secs => $1::double precision)
|
||
ORDER BY chat_finalize_deferred_at
|
||
LIMIT $2::int
|
||
`
|
||
|
||
type ListChatFinalizeDeferredExpiredParams struct {
|
||
GraceSecs float64 `json:"grace_secs"`
|
||
MaxPerTick int32 `json:"max_per_tick"`
|
||
}
|
||
|
||
// Deferred chat finalizations whose grace period elapsed without a daemon
|
||
// ack (dead or partitioned daemon). Batch-capped like the other sweeper
|
||
// queries so one tick can't monopolise the DB.
|
||
func (q *Queries) ListChatFinalizeDeferredExpired(ctx context.Context, arg ListChatFinalizeDeferredExpiredParams) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, listChatFinalizeDeferredExpired, arg.GraceSecs, arg.MaxPerTick)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listNonOwnerAgentsByRuntime = `-- name: ListNonOwnerAgentsByRuntime :many
|
||
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist FROM agent
|
||
WHERE runtime_id = $1
|
||
AND owner_id IS DISTINCT FROM $2
|
||
ORDER BY name ASC
|
||
`
|
||
|
||
type ListNonOwnerAgentsByRuntimeParams struct {
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
RuntimeOwnerID pgtype.UUID `json:"runtime_owner_id"`
|
||
}
|
||
|
||
// Returns every agent bound to a runtime whose owner differs from its owner,
|
||
// including hidden Builder execution carriers. This is the dependency plan shown before a public
|
||
// runtime is made private: owner-owned agents keep their binding, while every
|
||
// teammate-owned agent loses machine access.
|
||
func (q *Queries) ListNonOwnerAgentsByRuntime(ctx context.Context, arg ListNonOwnerAgentsByRuntimeParams) ([]Agent, error) {
|
||
rows, err := q.db.Query(ctx, listNonOwnerAgentsByRuntime, arg.RuntimeID, arg.RuntimeOwnerID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []Agent{}
|
||
for rows.Next() {
|
||
var i Agent
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listNonOwnerAgentsByRuntimeForUpdate = `-- name: ListNonOwnerAgentsByRuntimeForUpdate :many
|
||
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist FROM agent
|
||
WHERE runtime_id = $1
|
||
AND owner_id IS DISTINCT FROM $2
|
||
ORDER BY id
|
||
FOR UPDATE NOWAIT
|
||
`
|
||
|
||
type ListNonOwnerAgentsByRuntimeForUpdateParams struct {
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
RuntimeOwnerID pgtype.UUID `json:"runtime_owner_id"`
|
||
}
|
||
|
||
// Runtime access-change locking serializes new bindings. NOWAIT avoids waiting
|
||
// behind a writer that already holds an agent and needs the runtime row.
|
||
func (q *Queries) ListNonOwnerAgentsByRuntimeForUpdate(ctx context.Context, arg ListNonOwnerAgentsByRuntimeForUpdateParams) ([]Agent, error) {
|
||
rows, err := q.db.Query(ctx, listNonOwnerAgentsByRuntimeForUpdate, arg.RuntimeID, arg.RuntimeOwnerID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []Agent{}
|
||
for rows.Next() {
|
||
var i Agent
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listPendingDelegatedFailureRecoveries = `-- name: ListPendingDelegatedFailureRecoveries :many
|
||
SELECT recovery.id, recovery.issue_id, recovery.author_type, recovery.author_id, recovery.content, recovery.type, recovery.created_at, recovery.updated_at, recovery.parent_id, recovery.workspace_id, recovery.resolved_at, recovery.resolved_by_type, recovery.resolved_by_id, recovery.source_task_id, recovery.quick_action_id, recovery.via_plugin_id, recovery.revision, recovery.recovery_settled_at, recovery.deleted_at, recovery.suppressed_agent_ids
|
||
FROM comment recovery
|
||
JOIN agent_task_queue failed ON failed.id = recovery.source_task_id
|
||
JOIN agent_task_queue source ON source.id = failed.delegated_from_task_id
|
||
JOIN issue source_issue ON source_issue.id = source.issue_id
|
||
JOIN agent source_agent ON source_agent.id = source.agent_id
|
||
LEFT JOIN issue_status source_status
|
||
ON source_status.workspace_id = source_issue.workspace_id
|
||
AND source_status.key = source_issue.status
|
||
WHERE recovery.author_type = 'system'
|
||
AND recovery.type = 'progress_update'
|
||
AND recovery.source_task_id IS NOT NULL
|
||
AND recovery.recovery_settled_at IS NULL
|
||
-- A deleted recovery signal is withdrawn, even when replies keep its row.
|
||
AND recovery.deleted_at IS NULL
|
||
AND recovery.issue_id = source_issue.id
|
||
AND recovery.workspace_id = source_issue.workspace_id
|
||
AND failed.status = 'failed'
|
||
AND failed.delegated_from_task_id IS NOT NULL
|
||
AND failed.autopilot_run_id IS NULL
|
||
AND failed.trigger_evidence_kind IS DISTINCT FROM 'delegated_failure'
|
||
AND source.autopilot_run_id IS NULL
|
||
AND source.issue_id IS NOT NULL
|
||
AND source.agent_id <> failed.agent_id
|
||
-- Match canDispatchDelegatedFailureRecovery: lifecycle permits recovery only
|
||
-- for open work; parking belongs exclusively to the fixed Backlog status.
|
||
-- Built-ins resolve without catalog rows; unknown custom states stay pending.
|
||
AND source_issue.status <> 'backlog'
|
||
-- A coordinator waiting in Triage is the entry's proposed owner, not its
|
||
-- owner, so a worker failure must not wake it (MUL-7189 §2.3). Triage is not
|
||
-- a status, so it is a predicate of its own rather than a CASE arm.
|
||
AND source_issue.triage_state IS NULL
|
||
AND CASE
|
||
WHEN source_issue.status IN ('backlog', 'todo') THEN 'unstarted'
|
||
WHEN source_issue.status IN ('in_progress', 'in_review', 'blocked') THEN 'started'
|
||
WHEN source_issue.status = 'done' THEN 'done'
|
||
WHEN source_issue.status = 'cancelled' THEN 'closed'
|
||
ELSE source_status.category
|
||
END IN ('unstarted', 'started')
|
||
AND source_agent.archived_at IS NULL
|
||
AND source_agent.runtime_id IS NOT NULL
|
||
AND source_agent.workspace_id = source_issue.workspace_id
|
||
AND NOT EXISTS (
|
||
SELECT 1
|
||
FROM agent_task_queue retry
|
||
WHERE retry.parent_task_id = failed.id
|
||
AND retry.status <> 'cancelled'
|
||
)
|
||
AND NOT EXISTS (
|
||
SELECT 1
|
||
FROM agent_task_queue covering
|
||
WHERE covering.issue_id = source_issue.id
|
||
AND covering.agent_id = source.agent_id
|
||
AND (
|
||
recovery.id = ANY(covering.delivered_comment_ids)
|
||
OR (
|
||
(
|
||
covering.status IN ('queued', 'dispatched', 'running', 'waiting_local_directory')
|
||
OR (
|
||
covering.status = 'deferred'
|
||
AND covering.context->>'channel_issue_media_pending' = 'true'
|
||
)
|
||
)
|
||
AND (
|
||
covering.trigger_comment_id = recovery.id
|
||
OR recovery.id = ANY(covering.coalesced_comment_ids)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
ORDER BY recovery.created_at ASC, recovery.id ASC
|
||
LIMIT $1
|
||
`
|
||
|
||
// Durable outbox scan for platform recovery comments that are not yet owned by
|
||
// an executable task and have no terminal delivery receipt. Starting from the
|
||
// explicit recovery signal avoids retroactively waking unrelated historical
|
||
// delegated failures. A bounded runtime sweeper replays these comments after a
|
||
// transient dispatch error or process restart.
|
||
//
|
||
// recovery_settled_at is what keeps this scan from growing with history. Every
|
||
// other exclusion below is reversible — an issue can leave 'done', a cancelled
|
||
// retry can be superseded — so none of them can be frozen into the index
|
||
// predicate. A delivery receipt on a terminal task cannot be taken back, so
|
||
// that one condition is recorded as durable state instead of being re-proven
|
||
// through four joins and two NOT EXISTS subqueries on every tick. The predicate
|
||
// of idx_comment_delegated_failure_unsettled matches the first four conditions,
|
||
// narrowing the scan to unsettled signals. Reversible eligibility must still
|
||
// be checked before LIMIT so paused signals cannot starve executable ones.
|
||
func (q *Queries) ListPendingDelegatedFailureRecoveries(ctx context.Context, maxPerTick int32) ([]Comment, error) {
|
||
rows, err := q.db.Query(ctx, listPendingDelegatedFailureRecoveries, maxPerTick)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []Comment{}
|
||
for rows.Next() {
|
||
var i Comment
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.IssueID,
|
||
&i.AuthorType,
|
||
&i.AuthorID,
|
||
&i.Content,
|
||
&i.Type,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.ParentID,
|
||
&i.WorkspaceID,
|
||
&i.ResolvedAt,
|
||
&i.ResolvedByType,
|
||
&i.ResolvedByID,
|
||
&i.SourceTaskID,
|
||
&i.QuickActionID,
|
||
&i.ViaPluginID,
|
||
&i.Revision,
|
||
&i.RecoverySettledAt,
|
||
&i.DeletedAt,
|
||
&i.SuppressedAgentIds,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listPendingTasksByRuntime = `-- name: ListPendingTasksByRuntime :many
|
||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles FROM agent_task_queue
|
||
WHERE runtime_id = $1 AND status IN ('queued', 'dispatched')
|
||
ORDER BY priority DESC, created_at ASC
|
||
`
|
||
|
||
func (q *Queries) ListPendingTasksByRuntime(ctx context.Context, runtimeID pgtype.UUID) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, listPendingTasksByRuntime, runtimeID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listQueuedClaimCandidatesByRuntime = `-- name: ListQueuedClaimCandidatesByRuntime :many
|
||
SELECT atq.id, atq.agent_id, atq.issue_id, atq.status, atq.priority, atq.dispatched_at, atq.started_at, atq.completed_at, atq.result, atq.error, atq.created_at, atq.context, atq.runtime_id, atq.session_id, atq.work_dir, atq.trigger_comment_id, atq.chat_session_id, atq.autopilot_run_id, atq.attempt, atq.max_attempts, atq.parent_task_id, atq.failure_reason, atq.trigger_summary, atq.force_fresh_session, atq.is_leader_task, atq.wait_reason, atq.initiator_user_id, atq.handoff_note, atq.prepare_lease_expires_at, atq.squad_id, atq.runtime_mcp_overlay, atq.escalation_for_task_id, atq.fire_at, atq.originator_user_id, atq.runtime_connected_apps, atq.coalesced_comment_ids, atq.delivered_comment_ids, atq.chat_input_task_id, atq.chat_finalize_deferred_at, atq.originator_source, atq.delegated_from_task_id, atq.retry_of_task_id, atq.rerun_of_task_id, atq.rule_version_id, atq.trigger_evidence_kind, atq.trigger_evidence_ref_id, atq.accountable_user_id, atq.session_rollout_missing, atq.retired_session_id, atq.quick_actions_disabled, atq.regenerate_quick_actions_for, atq.branch_name, atq.durable_work_dir, atq.channel_context_revision, atq.comment_thread_id, atq.cancelled_by_type, atq.cancelled_by_id, atq.cancelled_by_name, atq.issue_snapshot, atq.workflow_step, atq.runtime_connector_handles FROM agent_task_queue atq
|
||
WHERE atq.runtime_id = $1
|
||
AND atq.status = 'queued'
|
||
AND (atq.context->>'wakeup_id' IS NULL OR EXISTS (SELECT 1 FROM issue_wakeup w WHERE w.id=(atq.context->>'wakeup_id')::uuid AND w.disabled_at IS NULL AND w.revision=(atq.context->>'wakeup_revision')::bigint))
|
||
AND EXISTS (
|
||
-- Keep this authorization fence in sync with ClaimAgentTask.
|
||
SELECT 1
|
||
FROM agent a
|
||
JOIN agent_runtime r ON r.id = atq.runtime_id
|
||
WHERE a.id = atq.agent_id
|
||
AND a.runtime_id = atq.runtime_id
|
||
AND (
|
||
r.visibility = 'public'
|
||
OR r.visibility = 'private'
|
||
)
|
||
)
|
||
ORDER BY atq.priority DESC, atq.created_at ASC
|
||
`
|
||
|
||
// Returns rows the runtime is authorized to attempt to claim. Status is restricted to
|
||
// 'queued' (in contrast to ListPendingTasksByRuntime which also includes
|
||
// 'dispatched') because dispatched rows are by definition already owned
|
||
// and cannot be re-claimed — including them in the candidate list pads
|
||
// the result with rows that always lose the per-(issue, agent) race in
|
||
// ClaimAgentTask, wasting CPU and a SELECT every poll cycle when the
|
||
// runtime is busy on a long-running task. Backed by the partial index
|
||
// idx_agent_task_queue_claim_candidates so the warm path is cheap.
|
||
func (q *Queries) ListQueuedClaimCandidatesByRuntime(ctx context.Context, runtimeID pgtype.UUID) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, listQueuedClaimCandidatesByRuntime, runtimeID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listQueuedClaimCandidatesByRuntimes = `-- name: ListQueuedClaimCandidatesByRuntimes :many
|
||
SELECT atq.id, atq.agent_id, atq.issue_id, atq.status, atq.priority, atq.dispatched_at, atq.started_at, atq.completed_at, atq.result, atq.error, atq.created_at, atq.context, atq.runtime_id, atq.session_id, atq.work_dir, atq.trigger_comment_id, atq.chat_session_id, atq.autopilot_run_id, atq.attempt, atq.max_attempts, atq.parent_task_id, atq.failure_reason, atq.trigger_summary, atq.force_fresh_session, atq.is_leader_task, atq.wait_reason, atq.initiator_user_id, atq.handoff_note, atq.prepare_lease_expires_at, atq.squad_id, atq.runtime_mcp_overlay, atq.escalation_for_task_id, atq.fire_at, atq.originator_user_id, atq.runtime_connected_apps, atq.coalesced_comment_ids, atq.delivered_comment_ids, atq.chat_input_task_id, atq.chat_finalize_deferred_at, atq.originator_source, atq.delegated_from_task_id, atq.retry_of_task_id, atq.rerun_of_task_id, atq.rule_version_id, atq.trigger_evidence_kind, atq.trigger_evidence_ref_id, atq.accountable_user_id, atq.session_rollout_missing, atq.retired_session_id, atq.quick_actions_disabled, atq.regenerate_quick_actions_for, atq.branch_name, atq.durable_work_dir, atq.channel_context_revision, atq.comment_thread_id, atq.cancelled_by_type, atq.cancelled_by_id, atq.cancelled_by_name, atq.issue_snapshot, atq.workflow_step, atq.runtime_connector_handles FROM agent_task_queue atq
|
||
WHERE atq.runtime_id = ANY($1::uuid[])
|
||
AND atq.status = 'queued'
|
||
AND (atq.context->>'wakeup_id' IS NULL OR EXISTS (SELECT 1 FROM issue_wakeup w WHERE w.id=(atq.context->>'wakeup_id')::uuid AND w.disabled_at IS NULL AND w.revision=(atq.context->>'wakeup_revision')::bigint))
|
||
AND EXISTS (
|
||
-- Keep this authorization fence in sync with ClaimAgentTask.
|
||
SELECT 1
|
||
FROM agent a
|
||
JOIN agent_runtime r ON r.id = atq.runtime_id
|
||
WHERE a.id = atq.agent_id
|
||
AND a.runtime_id = atq.runtime_id
|
||
AND (
|
||
r.visibility = 'public'
|
||
OR r.visibility = 'private'
|
||
)
|
||
)
|
||
ORDER BY atq.priority DESC, atq.created_at ASC
|
||
`
|
||
|
||
// Batch variant of ListQueuedClaimCandidatesByRuntime (MUL-4257): returns
|
||
// queued claim candidates across every runtime_id in the input set in ONE round
|
||
// trip, so a daemon can list candidates for all of its runtimes with a single
|
||
// query instead of one per runtime. Ordering matches the singular query
|
||
// (priority, then FIFO) so the batch claim loop keeps the same fairness. The
|
||
// runtime_id filter is served by the partial index
|
||
// idx_agent_task_queue_claim_candidates; the cross-runtime ORDER BY still needs
|
||
// a sort step (each runtime's slice is index-ordered, but merging several
|
||
// runtimes' rows into one priority/FIFO order is not). The per-machine
|
||
// candidate set is small, so this is cheap in practice.
|
||
func (q *Queries) ListQueuedClaimCandidatesByRuntimes(ctx context.Context, runtimeIds []pgtype.UUID) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, listQueuedClaimCandidatesByRuntimes, runtimeIds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listTasksByIssue = `-- name: ListTasksByIssue :many
|
||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles FROM agent_task_queue
|
||
WHERE issue_id = $1
|
||
ORDER BY created_at DESC
|
||
`
|
||
|
||
func (q *Queries) ListTasksByIssue(ctx context.Context, issueID pgtype.UUID) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, listTasksByIssue, issueID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listTerminalTasksWithConnectorHandles = `-- name: ListTerminalTasksWithConnectorHandles :many
|
||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles FROM agent_task_queue
|
||
WHERE runtime_connector_handles IS NOT NULL
|
||
AND status IN ('completed', 'failed', 'cancelled')
|
||
ORDER BY id
|
||
LIMIT 200
|
||
`
|
||
|
||
// Reaper input: terminal tasks whose connector resources were not released
|
||
// (best-effort release failed or the process died). Served by the partial
|
||
// index on runtime_connector_handles.
|
||
func (q *Queries) ListTerminalTasksWithConnectorHandles(ctx context.Context) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, listTerminalTasksWithConnectorHandles)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listUserAgentIDsByRuntime = `-- name: ListUserAgentIDsByRuntime :many
|
||
SELECT id FROM agent
|
||
WHERE runtime_id = $1 AND kind = 'user'
|
||
ORDER BY id
|
||
`
|
||
|
||
// Non-locking companion to ListUserAgentsByRuntimeForUpdate, for callers that
|
||
// must reason about retention GC without taking the teardown's locks.
|
||
//
|
||
// Archived rows are included deliberately, and that is the whole point: an
|
||
// archived agent can still own a non-terminal task, and gcRuntime counts those
|
||
// before it will delete a runtime. A read that filtered them would report a
|
||
// runtime as reclaimable when the sweeper is going to skip it.
|
||
func (q *Queries) ListUserAgentIDsByRuntime(ctx context.Context, runtimeID pgtype.UUID) ([]pgtype.UUID, error) {
|
||
rows, err := q.db.Query(ctx, listUserAgentIDsByRuntime, runtimeID)
|
||
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 listUserAgentsByRuntimeForUpdate = `-- name: ListUserAgentsByRuntimeForUpdate :many
|
||
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist FROM agent
|
||
WHERE runtime_id = $1 AND kind = 'user'
|
||
ORDER BY id
|
||
FOR UPDATE
|
||
`
|
||
|
||
// Locks active AND archived user agents before a runtime teardown. Locking only
|
||
// the active snapshot leaves a restore race: an archived row can become active
|
||
// after confirmation and then be silently unbound. Runtime/profile delete
|
||
// locks the parent runtime first (blocking new FK references), then calls this
|
||
// in stable ID order (blocking restore/archive/move on existing rows).
|
||
func (q *Queries) ListUserAgentsByRuntimeForUpdate(ctx context.Context, runtimeID pgtype.UUID) ([]Agent, error) {
|
||
rows, err := q.db.Query(ctx, listUserAgentsByRuntimeForUpdate, runtimeID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []Agent{}
|
||
for rows.Next() {
|
||
var i Agent
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listWorkspaceAgentTaskSnapshot = `-- name: ListWorkspaceAgentTaskSnapshot :many
|
||
SELECT atq.id, atq.agent_id, atq.issue_id, atq.status, atq.priority, atq.dispatched_at, atq.started_at, atq.completed_at, atq.result, atq.error, atq.created_at, atq.context, atq.runtime_id, atq.session_id, atq.work_dir, atq.trigger_comment_id, atq.chat_session_id, atq.autopilot_run_id, atq.attempt, atq.max_attempts, atq.parent_task_id, atq.failure_reason, atq.trigger_summary, atq.force_fresh_session, atq.is_leader_task, atq.wait_reason, atq.initiator_user_id, atq.handoff_note, atq.prepare_lease_expires_at, atq.squad_id, atq.runtime_mcp_overlay, atq.escalation_for_task_id, atq.fire_at, atq.originator_user_id, atq.runtime_connected_apps, atq.coalesced_comment_ids, atq.delivered_comment_ids, atq.chat_input_task_id, atq.chat_finalize_deferred_at, atq.originator_source, atq.delegated_from_task_id, atq.retry_of_task_id, atq.rerun_of_task_id, atq.rule_version_id, atq.trigger_evidence_kind, atq.trigger_evidence_ref_id, atq.accountable_user_id, atq.session_rollout_missing, atq.retired_session_id, atq.quick_actions_disabled, atq.regenerate_quick_actions_for, atq.branch_name, atq.durable_work_dir, atq.channel_context_revision, atq.comment_thread_id, atq.cancelled_by_type, atq.cancelled_by_id, atq.cancelled_by_name, atq.issue_snapshot, atq.workflow_step, atq.runtime_connector_handles FROM agent_task_queue atq
|
||
JOIN agent a ON a.id = atq.agent_id
|
||
WHERE a.workspace_id = $1
|
||
AND (atq.status IN ('queued', 'dispatched', 'running', 'waiting_local_directory')
|
||
OR (atq.status='deferred' AND atq.context->>'wakeup_id' IS NOT NULL))
|
||
|
||
UNION ALL
|
||
|
||
SELECT latest.id, latest.agent_id, latest.issue_id, latest.status, latest.priority, latest.dispatched_at, latest.started_at, latest.completed_at, latest.result, latest.error, latest.created_at, latest.context, latest.runtime_id, latest.session_id, latest.work_dir, latest.trigger_comment_id, latest.chat_session_id, latest.autopilot_run_id, latest.attempt, latest.max_attempts, latest.parent_task_id, latest.failure_reason, latest.trigger_summary, latest.force_fresh_session, latest.is_leader_task, latest.wait_reason, latest.initiator_user_id, latest.handoff_note, latest.prepare_lease_expires_at, latest.squad_id, latest.runtime_mcp_overlay, latest.escalation_for_task_id, latest.fire_at, latest.originator_user_id, latest.runtime_connected_apps, latest.coalesced_comment_ids, latest.delivered_comment_ids, latest.chat_input_task_id, latest.chat_finalize_deferred_at, latest.originator_source, latest.delegated_from_task_id, latest.retry_of_task_id, latest.rerun_of_task_id, latest.rule_version_id, latest.trigger_evidence_kind, latest.trigger_evidence_ref_id, latest.accountable_user_id, latest.session_rollout_missing, latest.retired_session_id, latest.quick_actions_disabled, latest.regenerate_quick_actions_for, latest.branch_name, latest.durable_work_dir, latest.channel_context_revision, latest.comment_thread_id, latest.cancelled_by_type, latest.cancelled_by_id, latest.cancelled_by_name, latest.issue_snapshot, latest.workflow_step, latest.runtime_connector_handles FROM agent a
|
||
JOIN LATERAL (
|
||
SELECT atq.id, atq.agent_id, atq.issue_id, atq.status, atq.priority, atq.dispatched_at, atq.started_at, atq.completed_at, atq.result, atq.error, atq.created_at, atq.context, atq.runtime_id, atq.session_id, atq.work_dir, atq.trigger_comment_id, atq.chat_session_id, atq.autopilot_run_id, atq.attempt, atq.max_attempts, atq.parent_task_id, atq.failure_reason, atq.trigger_summary, atq.force_fresh_session, atq.is_leader_task, atq.wait_reason, atq.initiator_user_id, atq.handoff_note, atq.prepare_lease_expires_at, atq.squad_id, atq.runtime_mcp_overlay, atq.escalation_for_task_id, atq.fire_at, atq.originator_user_id, atq.runtime_connected_apps, atq.coalesced_comment_ids, atq.delivered_comment_ids, atq.chat_input_task_id, atq.chat_finalize_deferred_at, atq.originator_source, atq.delegated_from_task_id, atq.retry_of_task_id, atq.rerun_of_task_id, atq.rule_version_id, atq.trigger_evidence_kind, atq.trigger_evidence_ref_id, atq.accountable_user_id, atq.session_rollout_missing, atq.retired_session_id, atq.quick_actions_disabled, atq.regenerate_quick_actions_for, atq.branch_name, atq.durable_work_dir, atq.channel_context_revision, atq.comment_thread_id, atq.cancelled_by_type, atq.cancelled_by_id, atq.cancelled_by_name, atq.issue_snapshot, atq.workflow_step, atq.runtime_connector_handles
|
||
FROM agent_task_queue atq
|
||
WHERE atq.agent_id = a.id
|
||
AND atq.status IN ('completed', 'failed')
|
||
ORDER BY atq.completed_at DESC NULLS LAST, atq.created_at DESC, atq.id DESC
|
||
LIMIT 1
|
||
) latest ON TRUE
|
||
WHERE a.workspace_id = $1
|
||
`
|
||
|
||
// Returns the tasks the front-end reads off one workspace-wide snapshot:
|
||
// - All active tasks (queued / dispatched / running / waiting_local_directory)
|
||
// — the current workload: working signal + counts (see derive-presence.ts).
|
||
// - Each agent's most recent OUTCOME task (completed / failed) — NOT part of
|
||
// presence since #1823; it is the "last activity" line the Squad hover card
|
||
// renders (agent-live-peek-card.tsx). Kept in this response because shipped
|
||
// desktop builds read it from here; see MUL-5436 for the plan to move it to
|
||
// a dedicated lazy endpoint.
|
||
//
|
||
// Cancelled tasks are excluded from the outcome half on purpose: cancel is a
|
||
// procedural signal ("attempt aborted"), not an outcome. It tells us nothing
|
||
// about whether the agent works, so it must NOT be allowed to mask a prior
|
||
// failure. Concretely: if an agent fails and then the user cancels the queued
|
||
// retry (or the parent issue closes and cascades cancels), the failure stays
|
||
// the agent's last real outcome.
|
||
//
|
||
// The outcome half is a per-agent LATERAL Top-1 rather than a workspace-wide
|
||
// DISTINCT ON: with idx_agent_task_queue_agent_terminal_latest (migration 233)
|
||
// each agent costs one ordered index scan + LIMIT 1, so the cost no longer
|
||
// grows with how much terminal history the workspace has accumulated. The
|
||
// (created_at, id) tie-break makes the pick deterministic when completed_at
|
||
// ties or is NULL — plain completed_at DESC returned an arbitrary row there.
|
||
// Deferred wakeup retries also remain visible as queued work.
|
||
//
|
||
// Both halves JOIN / scan agent because agent_task_queue has no workspace_id.
|
||
func (q *Queries) ListWorkspaceAgentTaskSnapshot(ctx context.Context, workspaceID pgtype.UUID) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, listWorkspaceAgentTaskSnapshot, workspaceID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const listWorkspaceWorkingAgents = `-- name: ListWorkspaceWorkingAgents :many
|
||
SELECT
|
||
a.id,
|
||
a.name,
|
||
a.avatar_url,
|
||
COUNT(*)::int AS running_task_count,
|
||
COALESCE(
|
||
ARRAY_AGG(DISTINCT atq.issue_id ORDER BY atq.issue_id)
|
||
FILTER (WHERE atq.issue_id IS NOT NULL),
|
||
ARRAY[]::uuid[]
|
||
)::uuid[] AS issue_ids
|
||
FROM agent a
|
||
JOIN agent_task_queue atq ON atq.agent_id = a.id
|
||
WHERE a.workspace_id = $1
|
||
AND a.kind = 'user'
|
||
AND a.archived_at IS NULL
|
||
AND atq.status = 'running'
|
||
AND (
|
||
$2::text = ''
|
||
OR ($2::text = 'chat' AND atq.chat_session_id IS NOT NULL)
|
||
OR (
|
||
$2::text = 'autopilot'
|
||
AND atq.chat_session_id IS NULL
|
||
AND atq.autopilot_run_id IS NOT NULL
|
||
)
|
||
OR (
|
||
$2::text = 'issue'
|
||
AND atq.chat_session_id IS NULL
|
||
AND atq.autopilot_run_id IS NULL
|
||
AND atq.issue_id IS NOT NULL
|
||
)
|
||
)
|
||
AND (
|
||
$3::text = ''
|
||
OR EXISTS (
|
||
SELECT 1
|
||
FROM issue i
|
||
WHERE i.id = atq.issue_id
|
||
AND i.workspace_id = a.workspace_id
|
||
AND (
|
||
(
|
||
$3::text IN ('assigned', 'any')
|
||
AND i.assignee_type = 'member'
|
||
AND i.assignee_id = $4::uuid
|
||
)
|
||
OR (
|
||
$3::text IN ('created', 'any')
|
||
AND i.creator_type = 'member'
|
||
AND i.creator_id = $4::uuid
|
||
)
|
||
OR (
|
||
$3::text IN ('involved', 'any')
|
||
AND (
|
||
(
|
||
i.assignee_type = 'agent'
|
||
AND EXISTS (
|
||
SELECT 1
|
||
FROM agent owned_agent
|
||
WHERE owned_agent.id = i.assignee_id
|
||
AND owned_agent.workspace_id = a.workspace_id
|
||
AND owned_agent.owner_id = $4::uuid
|
||
)
|
||
)
|
||
OR (
|
||
i.assignee_type = 'squad'
|
||
AND EXISTS (
|
||
SELECT 1
|
||
FROM squad s
|
||
WHERE s.id = i.assignee_id
|
||
AND s.workspace_id = a.workspace_id
|
||
AND (
|
||
EXISTS (
|
||
SELECT 1
|
||
FROM squad_member sm
|
||
WHERE sm.squad_id = s.id
|
||
AND sm.member_type = 'member'
|
||
AND sm.member_id = $4::uuid
|
||
)
|
||
OR EXISTS (
|
||
SELECT 1
|
||
FROM agent leader
|
||
WHERE leader.id = s.leader_id
|
||
AND leader.workspace_id = a.workspace_id
|
||
AND leader.owner_id = $4::uuid
|
||
)
|
||
OR EXISTS (
|
||
SELECT 1
|
||
FROM squad_member sm
|
||
JOIN agent owned_member ON owned_member.id = sm.member_id
|
||
WHERE sm.squad_id = s.id
|
||
AND sm.member_type = 'agent'
|
||
AND owned_member.workspace_id = a.workspace_id
|
||
AND owned_member.owner_id = $4::uuid
|
||
)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
AND (
|
||
$5::uuid IS NULL
|
||
OR EXISTS (
|
||
SELECT 1
|
||
FROM issue child
|
||
WHERE child.id = atq.issue_id
|
||
AND child.workspace_id = a.workspace_id
|
||
AND child.parent_issue_id = $5::uuid
|
||
)
|
||
)
|
||
GROUP BY a.id, a.name, a.avatar_url, a.created_at
|
||
ORDER BY a.created_at ASC
|
||
`
|
||
|
||
type ListWorkspaceWorkingAgentsParams struct {
|
||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||
WorkType string `json:"work_type"`
|
||
MineRelation string `json:"mine_relation"`
|
||
MemberID pgtype.UUID `json:"member_id"`
|
||
ParentIssueID pgtype.UUID `json:"parent_issue_id"`
|
||
}
|
||
|
||
type ListWorkspaceWorkingAgentsRow struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
Name string `json:"name"`
|
||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||
RunningTaskCount int32 `json:"running_task_count"`
|
||
IssueIds []pgtype.UUID `json:"issue_ids"`
|
||
}
|
||
|
||
// Workspace-level source for consumers that show currently working agents.
|
||
// One row per visible, user-authored agent with at least one task that has
|
||
// actually started running. work_type is optional (empty = every source);
|
||
// source-specific reads use the same precedence as computeTaskKind:
|
||
// chat > autopilot > issue. "issue" intentionally groups direct and
|
||
// comment-triggered issue work. Quick-create work is present only in the
|
||
// unfiltered projection because it has no source FK yet. mine_relation is
|
||
// optional (empty = workspace); when set it narrows issue work to the
|
||
// authenticated member's My Issues relation. parent_issue_id is optional
|
||
// (NULL = workspace); when set it narrows issue work to the direct children
|
||
// of that issue, so an issue detail's sub-issue header reads the same
|
||
// projection as the Issues list header instead of deriving its own count.
|
||
func (q *Queries) ListWorkspaceWorkingAgents(ctx context.Context, arg ListWorkspaceWorkingAgentsParams) ([]ListWorkspaceWorkingAgentsRow, error) {
|
||
rows, err := q.db.Query(ctx, listWorkspaceWorkingAgents,
|
||
arg.WorkspaceID,
|
||
arg.WorkType,
|
||
arg.MineRelation,
|
||
arg.MemberID,
|
||
arg.ParentIssueID,
|
||
)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []ListWorkspaceWorkingAgentsRow{}
|
||
for rows.Next() {
|
||
var i ListWorkspaceWorkingAgentsRow
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RunningTaskCount,
|
||
&i.IssueIds,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const lockAgentForAutopilotAssignment = `-- name: LockAgentForAutopilotAssignment :one
|
||
SELECT id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist FROM agent
|
||
WHERE id = $1 AND workspace_id = $2 AND kind = 'user'
|
||
FOR SHARE
|
||
`
|
||
|
||
type LockAgentForAutopilotAssignmentParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||
}
|
||
|
||
// Serializes creating, retargeting, or resuming an active Autopilot with
|
||
// Runtime teardown. Teardown takes FOR UPDATE on this same Agent row before it
|
||
// clears runtime_id and pauses matching Autopilots. FOR SHARE also stabilizes
|
||
// runtime_id and archived_at against concurrent ordinary Agent updates. The
|
||
// shared lock makes the
|
||
// two outcomes exhaustive:
|
||
// - assignment commits first, then teardown sees and pauses it; or
|
||
// - teardown commits first, then assignment re-reads runtime_id=NULL and
|
||
// rejects the active Autopilot.
|
||
func (q *Queries) LockAgentForAutopilotAssignment(ctx context.Context, arg LockAgentForAutopilotAssignmentParams) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, lockAgentForAutopilotAssignment, arg.ID, arg.WorkspaceID)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const lockAgentTaskStartClaim = `-- name: LockAgentTaskStartClaim :one
|
||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles FROM agent_task_queue
|
||
WHERE id = $1 AND runtime_id = $2 AND dispatched_at = $3
|
||
AND status IN ('dispatched', 'waiting_local_directory', 'running')
|
||
FOR UPDATE
|
||
`
|
||
|
||
type LockAgentTaskStartClaimParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
DispatchedAt pgtype.Timestamptz `json:"dispatched_at"`
|
||
}
|
||
|
||
// Serialize start/replay with reclaim and cancellation. A stale delivery must
|
||
// never start or acknowledge a newer claim, even on the same runtime.
|
||
func (q *Queries) LockAgentTaskStartClaim(ctx context.Context, arg LockAgentTaskStartClaimParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, lockAgentTaskStartClaim, arg.ID, arg.RuntimeID, arg.DispatchedAt)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const markAgentTaskWaitingLocalDirectory = `-- name: MarkAgentTaskWaitingLocalDirectory :one
|
||
UPDATE agent_task_queue
|
||
SET status = 'waiting_local_directory',
|
||
wait_reason = $2,
|
||
prepare_lease_expires_at = now() + make_interval(secs => $3::double precision)
|
||
WHERE id = $1 AND status = 'dispatched'
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type MarkAgentTaskWaitingLocalDirectoryParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
WaitReason pgtype.Text `json:"wait_reason"`
|
||
PrepareLeaseSecs float64 `json:"prepare_lease_secs"`
|
||
}
|
||
|
||
// Transitions a freshly-dispatched task into 'waiting_local_directory' while
|
||
// the daemon waits for another in-flight task to release the path lock on a
|
||
// project_resource of type local_directory. wait_reason carries a short
|
||
// human-readable hint (typically the contested path) that the UI surfaces
|
||
// alongside the status.
|
||
//
|
||
// The CHECK only allows the transition from 'dispatched' so a daemon can't
|
||
// mark an already-running or terminal task as waiting; the StartAgentTask
|
||
// mutation handles the reverse transition once the lock is acquired.
|
||
func (q *Queries) MarkAgentTaskWaitingLocalDirectory(ctx context.Context, arg MarkAgentTaskWaitingLocalDirectoryParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, markAgentTaskWaitingLocalDirectory, arg.ID, arg.WaitReason, arg.PrepareLeaseSecs)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const markChatFinalizeDeferred = `-- name: MarkChatFinalizeDeferred :one
|
||
UPDATE agent_task_queue
|
||
SET chat_finalize_deferred_at = now()
|
||
WHERE id = $1
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
// Arms the deferred chat-finalize marker for a cancelled chat task whose
|
||
// empty-transcript judgment must wait for the daemon's flush ack (#5219).
|
||
func (q *Queries) MarkChatFinalizeDeferred(ctx context.Context, id pgtype.UUID) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, markChatFinalizeDeferred, id)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const mergeCommentIntoPendingTask = `-- name: MergeCommentIntoPendingTask :one
|
||
UPDATE agent_task_queue
|
||
SET coalesced_comment_ids = (
|
||
SELECT COALESCE(array_agg(DISTINCT e), '{}')
|
||
FROM unnest(array_append(coalesced_comment_ids, trigger_comment_id)) AS e
|
||
WHERE e IS NOT NULL AND e <> $1::uuid
|
||
),
|
||
trigger_comment_id = $1::uuid,
|
||
trigger_summary = COALESCE($2, trigger_summary),
|
||
-- Re-attribution is ATOMIC (MUL-4302): folding a newly-arrived comment moves the
|
||
-- WHOLE attribution snapshot to that comment's human — person columns, source
|
||
-- label, delegation lineage, rule version, and evidence — computed by the caller
|
||
-- as one attribution.Result. Re-stamping only the person columns would leave a
|
||
-- run showing B accountable while still pointing at A's stale source / evidence /
|
||
-- level. accountable comes from the resolved Result (finalizeAttribution already
|
||
-- guaranteed originator ⟹ accountable == originator; the cross-column CHECK backs it).
|
||
originator_user_id = $3::uuid,
|
||
accountable_user_id = $4::uuid,
|
||
originator_source = $5,
|
||
delegated_from_task_id = $6::uuid,
|
||
rule_version_id = $7::uuid,
|
||
trigger_evidence_kind = $8,
|
||
trigger_evidence_ref_id = $9::uuid,
|
||
runtime_mcp_overlay = $10,
|
||
runtime_connected_apps = $11,
|
||
runtime_connector_handles = $12
|
||
WHERE id = (
|
||
SELECT t.id FROM agent_task_queue t
|
||
WHERE t.context->>'wakeup_id' IS NULL AND t.issue_id = $13
|
||
AND t.agent_id = $14
|
||
AND t.comment_thread_id IS NOT DISTINCT FROM comment_thread_root_id($1::uuid)
|
||
AND (
|
||
t.status = 'queued'
|
||
OR (t.status = 'deferred' AND t.context->>'channel_issue_media_pending' = 'true')
|
||
)
|
||
-- Head-scoped (TEN-356, #5914): never fold across HEADs. The physical
|
||
-- unique index is per (issue_id, agent_id, thread), so an insert-race loser can
|
||
-- collide with a pending task stamped for a DIFFERENT head_sha; merging
|
||
-- into it would give a new-HEAD comment old-HEAD review coverage. Empty/
|
||
-- absent head_sha (no linked PR) matches any task, preserving coalescing.
|
||
AND (
|
||
COALESCE($15::text, '') = ''
|
||
OR t.context->>'head_sha' = $15::text
|
||
)
|
||
ORDER BY t.created_at DESC
|
||
LIMIT 1
|
||
)
|
||
RETURNING id, coalesced_comment_ids
|
||
`
|
||
|
||
type MergeCommentIntoPendingTaskParams struct {
|
||
NewTriggerCommentID pgtype.UUID `json:"new_trigger_comment_id"`
|
||
NewTriggerSummary pgtype.Text `json:"new_trigger_summary"`
|
||
NewOriginatorUserID pgtype.UUID `json:"new_originator_user_id"`
|
||
NewAccountableUserID pgtype.UUID `json:"new_accountable_user_id"`
|
||
NewOriginatorSource pgtype.Text `json:"new_originator_source"`
|
||
NewDelegatedFromTaskID pgtype.UUID `json:"new_delegated_from_task_id"`
|
||
NewRuleVersionID pgtype.UUID `json:"new_rule_version_id"`
|
||
NewTriggerEvidenceKind pgtype.Text `json:"new_trigger_evidence_kind"`
|
||
NewTriggerEvidenceRefID pgtype.UUID `json:"new_trigger_evidence_ref_id"`
|
||
NewRuntimeMcpOverlay []byte `json:"new_runtime_mcp_overlay"`
|
||
NewRuntimeConnectedApps []byte `json:"new_runtime_connected_apps"`
|
||
NewRuntimeConnectorHandles []byte `json:"new_runtime_connector_handles"`
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
HeadSha pgtype.Text `json:"head_sha"`
|
||
}
|
||
|
||
type MergeCommentIntoPendingTaskRow struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
CoalescedCommentIds []pgtype.UUID `json:"coalesced_comment_ids"`
|
||
}
|
||
|
||
// MUL-4195: fold a newly-arrived comment into an existing task for (issue,
|
||
// agent) that has NOT yet been claimed, instead of letting the
|
||
// HasPendingTaskForIssueAndAgent dedup silently DROP it. The task's prior
|
||
// trigger_comment_id becomes a coalesced ("also cover") comment and
|
||
// @new_trigger_comment_id becomes the new trigger, so the injected prompt shows
|
||
// the latest deliberate instruction while the single run is still told to
|
||
// address every folded comment.
|
||
//
|
||
// Target is restricted to a pre-claim task on purpose (MUL-4195 review rounds
|
||
// 2–4). This merge is reached when HasPendingTaskForIssueAndAgent matched a
|
||
// 'queued'/'dispatched' task, or the channel-media deferred task described
|
||
// below. 'dispatched' is deliberately NOT a
|
||
// target: a dispatched / waiting_local_directory / running task has already had
|
||
// its claim response built. Folding afterward would add a planned id that is
|
||
// absent from that response's delivered_comment_ids receipt; completion
|
||
// reconciliation handles it instead. Ordinary 'deferred' rows remain excluded:
|
||
// assignee-fallback escalations have their own fire_at lifecycle and may coexist
|
||
// with a queued primary. The one exception is a task explicitly marked
|
||
// channel_issue_media_pending. It is the issue's sole assigned-agent task and
|
||
// has not been claimable yet, so a new comment must merge into it instead of
|
||
// creating a competing queued task. coalesced_comment_ids remains the pre-claim
|
||
// plan; the claim path records the actual embedded subset in
|
||
// delivered_comment_ids.
|
||
//
|
||
// Recompute-on-merge (MUL-4195 review must-fix #1): originator_user_id,
|
||
// runtime_mcp_overlay and runtime_connected_apps are re-stamped to the NEW
|
||
// trigger comment's originator (computed by the caller). Earlier this only
|
||
// repointed the trigger and kept the old originator's overlay/attribution, so a
|
||
// run answering user B's comment could execute under user A's connected-app
|
||
// capabilities and audit identity. Re-stamping means the single coalescing run
|
||
// carries the latest deliberate instruction's originator and the matching
|
||
// overlay — no cross-user capability bleed, no stale attribution. This also
|
||
// removes the previous originator gate + fresh-enqueue fallback, which could not
|
||
// create a second task anyway (the idx_one_pending_task_per_issue_agent unique
|
||
// index allows only one queued/dispatched task per (issue, agent)) and therefore
|
||
// silently dropped the mismatched-originator comment.
|
||
//
|
||
// Returns pgx.ErrNoRows when no queued task exists (it was claimed/started
|
||
// between the dedup check and this call, or the only task is already
|
||
// dispatched/running). The caller must NOT blindly enqueue a fresh task in that
|
||
// case — a dispatched sibling would trip the unique index — it defers to
|
||
// completion reconciliation unless no active task exists at all.
|
||
func (q *Queries) MergeCommentIntoPendingTask(ctx context.Context, arg MergeCommentIntoPendingTaskParams) (MergeCommentIntoPendingTaskRow, error) {
|
||
row := q.db.QueryRow(ctx, mergeCommentIntoPendingTask,
|
||
arg.NewTriggerCommentID,
|
||
arg.NewTriggerSummary,
|
||
arg.NewOriginatorUserID,
|
||
arg.NewAccountableUserID,
|
||
arg.NewOriginatorSource,
|
||
arg.NewDelegatedFromTaskID,
|
||
arg.NewRuleVersionID,
|
||
arg.NewTriggerEvidenceKind,
|
||
arg.NewTriggerEvidenceRefID,
|
||
arg.NewRuntimeMcpOverlay,
|
||
arg.NewRuntimeConnectedApps,
|
||
arg.NewRuntimeConnectorHandles,
|
||
arg.IssueID,
|
||
arg.AgentID,
|
||
arg.HeadSha,
|
||
)
|
||
var i MergeCommentIntoPendingTaskRow
|
||
err := row.Scan(&i.ID, &i.CoalescedCommentIds)
|
||
return i, err
|
||
}
|
||
|
||
const mergeDelegatedFailureCommentIntoPendingTask = `-- name: MergeDelegatedFailureCommentIntoPendingTask :one
|
||
UPDATE agent_task_queue
|
||
SET coalesced_comment_ids = (
|
||
SELECT COALESCE(array_agg(DISTINCT e), '{}')
|
||
FROM unnest(array_append(coalesced_comment_ids, trigger_comment_id)) AS e
|
||
WHERE e IS NOT NULL AND e <> $1::uuid
|
||
),
|
||
trigger_comment_id = $1::uuid,
|
||
trigger_summary = $2
|
||
WHERE id = (
|
||
SELECT t.id FROM agent_task_queue t
|
||
WHERE t.context->>'wakeup_id' IS NULL AND t.issue_id = $3
|
||
AND t.agent_id = $4
|
||
AND t.comment_thread_id IS NOT DISTINCT FROM comment_thread_root_id($1::uuid)
|
||
AND (
|
||
t.status = 'queued'
|
||
OR (t.status = 'deferred' AND t.context->>'channel_issue_media_pending' = 'true')
|
||
)
|
||
AND t.trigger_comment_id IS DISTINCT FROM $1::uuid
|
||
AND NOT ($1::uuid = ANY(t.coalesced_comment_ids))
|
||
ORDER BY t.created_at DESC
|
||
LIMIT 1
|
||
)
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type MergeDelegatedFailureCommentIntoPendingTaskParams struct {
|
||
CommentID pgtype.UUID `json:"comment_id"`
|
||
TriggerSummary pgtype.Text `json:"trigger_summary"`
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
}
|
||
|
||
// A delegated failure is a platform-owned input, not a new human instruction:
|
||
// fold it into the coordinator's pre-claim task without replacing that task's
|
||
// attribution snapshot. The recovery comment is the newest input, so it becomes
|
||
// the trigger and the prior trigger joins the coalesced plan. This preserves the
|
||
// claim/prompt invariant that trigger_comment_id names the newest comment while
|
||
// retaining the pending task's original human authority and connected apps.
|
||
func (q *Queries) MergeDelegatedFailureCommentIntoPendingTask(ctx context.Context, arg MergeDelegatedFailureCommentIntoPendingTaskParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, mergeDelegatedFailureCommentIntoPendingTask,
|
||
arg.CommentID,
|
||
arg.TriggerSummary,
|
||
arg.IssueID,
|
||
arg.AgentID,
|
||
)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const nextDeferredTaskFireAtForRuntimes = `-- name: NextDeferredTaskFireAtForRuntimes :one
|
||
SELECT MIN(fire_at)::timestamptz
|
||
FROM agent_task_queue t
|
||
WHERE t.runtime_id = ANY($1::uuid[])
|
||
AND t.status = 'deferred'
|
||
AND EXISTS (
|
||
SELECT 1 FROM agent_runtime r
|
||
WHERE r.id = t.runtime_id
|
||
AND r.status = 'online'
|
||
AND COALESCE(r.last_seen_at, r.updated_at) >=
|
||
now() - make_interval(secs => $2::double precision)
|
||
)
|
||
AND (
|
||
t.fire_at > now()
|
||
OR NOT EXISTS (
|
||
SELECT 1 FROM agent_task_queue occupant
|
||
WHERE occupant.issue_id = t.issue_id
|
||
AND occupant.agent_id = t.agent_id
|
||
AND occupant.comment_thread_id IS NOT DISTINCT FROM t.comment_thread_id
|
||
AND occupant.id <> t.id
|
||
AND (
|
||
occupant.status IN ('queued', 'dispatched')
|
||
OR (occupant.status = 'deferred' AND occupant.context->>'channel_issue_media_pending' = 'true')
|
||
)
|
||
)
|
||
)
|
||
`
|
||
|
||
type NextDeferredTaskFireAtForRuntimesParams struct {
|
||
RuntimeIds []pgtype.UUID `json:"runtime_ids"`
|
||
RuntimeStaleSecs float64 `json:"runtime_stale_secs"`
|
||
}
|
||
|
||
// Returns the next future deferred task for a daemon's authorized runtime set,
|
||
// or an eligible task that crossed fire_at during this claim. Overdue tasks
|
||
// whose runtime is offline/stale or that are blocked by an existing issue+agent
|
||
// occupant are omitted so they cannot cause a tight poll loop. Keep both fences
|
||
// in sync with PromoteDueDeferredTasksForRuntimes: a task that cannot be
|
||
// promoted must not advertise an immediate follow-up claim. The response
|
||
// converts the timestamp to a relative delay, avoiding any dependency on
|
||
// daemon/server clock synchronization.
|
||
func (q *Queries) NextDeferredTaskFireAtForRuntimes(ctx context.Context, arg NextDeferredTaskFireAtForRuntimesParams) (pgtype.Timestamptz, error) {
|
||
row := q.db.QueryRow(ctx, nextDeferredTaskFireAtForRuntimes, arg.RuntimeIds, arg.RuntimeStaleSecs)
|
||
var column_1 pgtype.Timestamptz
|
||
err := row.Scan(&column_1)
|
||
return column_1, err
|
||
}
|
||
|
||
const promoteDeferredChannelIssueTask = `-- name: PromoteDeferredChannelIssueTask :one
|
||
UPDATE agent_task_queue
|
||
SET status = 'queued', fire_at = NULL
|
||
WHERE id = $1 AND issue_id IS NOT NULL AND status = 'deferred'
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
// Early promotion is idempotent at the service layer: a task already promoted
|
||
// by the fire_at sweeper no longer matches and is treated as settled.
|
||
func (q *Queries) PromoteDeferredChannelIssueTask(ctx context.Context, id pgtype.UUID) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, promoteDeferredChannelIssueTask, id)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const promoteDueDeferredTasksForRuntime = `-- name: PromoteDueDeferredTasksForRuntime :many
|
||
WITH due AS (
|
||
SELECT t.id,
|
||
t.issue_id,
|
||
row_number() OVER (
|
||
PARTITION BY t.issue_id, t.agent_id, t.comment_thread_id
|
||
ORDER BY t.priority DESC, t.created_at ASC, t.id
|
||
) AS rn
|
||
FROM agent_task_queue t
|
||
WHERE t.runtime_id = $1
|
||
AND t.status = 'deferred'
|
||
AND t.fire_at <= now()
|
||
AND EXISTS (
|
||
SELECT 1 FROM agent_runtime r
|
||
WHERE r.id = t.runtime_id
|
||
AND r.status = 'online'
|
||
AND COALESCE(r.last_seen_at, r.updated_at) >=
|
||
now() - make_interval(secs => $2::double precision)
|
||
)
|
||
AND NOT EXISTS (
|
||
SELECT 1 FROM agent_task_queue occupant
|
||
WHERE occupant.issue_id = t.issue_id
|
||
AND occupant.agent_id = t.agent_id
|
||
AND occupant.comment_thread_id IS NOT DISTINCT FROM t.comment_thread_id
|
||
AND occupant.id <> t.id
|
||
AND (
|
||
occupant.status IN ('queued', 'dispatched')
|
||
OR (occupant.status = 'deferred' AND occupant.context->>'channel_issue_media_pending' = 'true')
|
||
)
|
||
)
|
||
)
|
||
UPDATE agent_task_queue
|
||
SET status = 'queued'
|
||
WHERE id IN (SELECT id FROM due WHERE issue_id IS NULL OR rn = 1)
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type PromoteDueDeferredTasksForRuntimeParams struct {
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
RuntimeStaleSecs float64 `json:"runtime_stale_secs"`
|
||
}
|
||
|
||
// Promotion is fenced against the single queued/dispatched slot
|
||
// idx_one_pending_task_per_issue_agent_thread allows per (issue, agent, thread). A deferred
|
||
// row is NOT covered by that index, so it can legitimately coexist with a queued
|
||
// one — a manual rerun enqueued behind a running task, plus the deferred retry
|
||
// that task's failure armed (runtime_offline, provider_network's final attempt).
|
||
// Flipping such a row to 'queued' unconditionally violates the index, and because
|
||
// the claim loop promotes before it claims, that error blocked every claim on the
|
||
// runtime — including the rerun the operator was waiting for.
|
||
//
|
||
// Two fences: skip a row whose (issue, agent, thread) slot is already occupied, and
|
||
// promote at most ONE row per (issue, agent, thread) so a single statement cannot collide
|
||
// with itself. A skipped row stays deferred with fire_at in the past and is
|
||
// promoted by a later tick once the slot frees, so nothing is lost — the human's
|
||
// rerun simply goes first. Chat / quick-create rows (issue_id NULL) are outside
|
||
// the index and bypass both fences.
|
||
func (q *Queries) PromoteDueDeferredTasksForRuntime(ctx context.Context, arg PromoteDueDeferredTasksForRuntimeParams) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, promoteDueDeferredTasksForRuntime, arg.RuntimeID, arg.RuntimeStaleSecs)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const promoteDueDeferredTasksForRuntimes = `-- name: PromoteDueDeferredTasksForRuntimes :many
|
||
WITH due AS (
|
||
SELECT t.id,
|
||
t.issue_id,
|
||
row_number() OVER (
|
||
PARTITION BY t.issue_id, t.agent_id, t.comment_thread_id
|
||
ORDER BY t.priority DESC, t.created_at ASC, t.id
|
||
) AS rn
|
||
FROM agent_task_queue t
|
||
WHERE t.runtime_id = ANY($1::uuid[])
|
||
AND t.status = 'deferred'
|
||
AND t.fire_at <= now()
|
||
AND EXISTS (
|
||
SELECT 1 FROM agent_runtime r
|
||
WHERE r.id = t.runtime_id
|
||
AND r.status = 'online'
|
||
AND COALESCE(r.last_seen_at, r.updated_at) >=
|
||
now() - make_interval(secs => $2::double precision)
|
||
)
|
||
AND NOT EXISTS (
|
||
SELECT 1 FROM agent_task_queue occupant
|
||
WHERE occupant.issue_id = t.issue_id
|
||
AND occupant.agent_id = t.agent_id
|
||
AND occupant.comment_thread_id IS NOT DISTINCT FROM t.comment_thread_id
|
||
AND occupant.id <> t.id
|
||
AND (
|
||
occupant.status IN ('queued', 'dispatched')
|
||
OR (occupant.status = 'deferred' AND occupant.context->>'channel_issue_media_pending' = 'true')
|
||
)
|
||
)
|
||
)
|
||
UPDATE agent_task_queue
|
||
SET status = 'queued'
|
||
WHERE id IN (SELECT id FROM due WHERE issue_id IS NULL OR rn = 1)
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type PromoteDueDeferredTasksForRuntimesParams struct {
|
||
RuntimeIds []pgtype.UUID `json:"runtime_ids"`
|
||
RuntimeStaleSecs float64 `json:"runtime_stale_secs"`
|
||
}
|
||
|
||
// Batch variant of PromoteDueDeferredTasksForRuntime (MUL-4257): promotes all
|
||
// due deferred tasks across the runtime set in one UPDATE. Carries the same two
|
||
// fences as the singular query; see its comment for why.
|
||
func (q *Queries) PromoteDueDeferredTasksForRuntimes(ctx context.Context, arg PromoteDueDeferredTasksForRuntimesParams) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, promoteDueDeferredTasksForRuntimes, arg.RuntimeIds, arg.RuntimeStaleSecs)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const rebindAgentBuilderRuntime = `-- name: RebindAgentBuilderRuntime :one
|
||
UPDATE agent
|
||
SET runtime_id = $1,
|
||
runtime_mode = $2,
|
||
model = $3,
|
||
updated_at = now()
|
||
WHERE id = $4 AND kind = 'system' AND system_key LIKE 'agent_builder:%'
|
||
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist
|
||
`
|
||
|
||
type RebindAgentBuilderRuntimeParams struct {
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
RuntimeMode string `json:"runtime_mode"`
|
||
Model pgtype.Text `json:"model"`
|
||
ID pgtype.UUID `json:"id"`
|
||
}
|
||
|
||
// Re-points a builder carrier at another runtime mid-conversation. The carrier
|
||
// is what SendDirectChatMessage reads to stamp a chat task's runtime_id, so this
|
||
// UPDATE is the only thing that actually moves subsequent replies; the live-draft
|
||
// picker alone never did. model is reset wholesale because model ids are
|
||
// per-runtime — the new runtime resolves its own default instead of inheriting an
|
||
// id it may not serve. The kind/system_key guard mirrors DeleteSystemAgentByID so
|
||
// this path can never touch a user-authored agent.
|
||
//
|
||
// Callers MUST hold LockChatSessionForRuntimeBind on the owning chat_session for
|
||
// the whole transaction, otherwise a concurrent send can stamp a task with the
|
||
// pre-switch runtime after the pending-task check has already passed (MUL-5163).
|
||
//
|
||
// chat_session.runtime_id is deliberately left untouched: the daemon only resumes
|
||
// a stored provider session when chat_session.runtime_id matches the claiming
|
||
// task's runtime (see the chat claim path), so leaving the old pointer in place is
|
||
// exactly what makes the new runtime start a fresh provider session.
|
||
func (q *Queries) RebindAgentBuilderRuntime(ctx context.Context, arg RebindAgentBuilderRuntimeParams) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, rebindAgentBuilderRuntime,
|
||
arg.RuntimeID,
|
||
arg.RuntimeMode,
|
||
arg.Model,
|
||
arg.ID,
|
||
)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const reclaimStaleDispatchedTaskForRuntime = `-- name: ReclaimStaleDispatchedTaskForRuntime :one
|
||
UPDATE agent_task_queue
|
||
SET dispatched_at = now(),
|
||
prepare_lease_expires_at = now() + make_interval(secs => $2::double precision)
|
||
WHERE id = (
|
||
SELECT atq.id FROM agent_task_queue atq
|
||
WHERE atq.runtime_id = $1
|
||
AND atq.status = 'dispatched'
|
||
AND atq.started_at IS NULL
|
||
AND atq.dispatched_at < now() - make_interval(secs => $3::double precision)
|
||
AND (atq.prepare_lease_expires_at IS NULL OR atq.prepare_lease_expires_at < now())
|
||
AND EXISTS (
|
||
-- Keep the dispatched-reclaim owner fence intentionally stricter
|
||
-- than the queued claim carve-out below.
|
||
SELECT 1
|
||
FROM agent a
|
||
JOIN agent_runtime r ON r.id = atq.runtime_id
|
||
WHERE a.id = atq.agent_id
|
||
AND a.runtime_id = atq.runtime_id
|
||
AND (
|
||
r.visibility = 'public'
|
||
OR (
|
||
r.visibility = 'private'
|
||
AND (
|
||
r.owner_id IS NULL
|
||
OR a.owner_id IS NULL
|
||
OR r.owner_id = a.owner_id
|
||
)
|
||
)
|
||
)
|
||
AND r.status = 'online'
|
||
AND COALESCE(r.last_seen_at, r.updated_at) >=
|
||
now() - make_interval(secs => $4::double precision)
|
||
)
|
||
ORDER BY atq.priority DESC, atq.dispatched_at ASC
|
||
LIMIT 1
|
||
FOR UPDATE SKIP LOCKED
|
||
)
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type ReclaimStaleDispatchedTaskForRuntimeParams struct {
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
PrepareLeaseSecs float64 `json:"prepare_lease_secs"`
|
||
ClaimRecoverySecs float64 `json:"claim_recovery_secs"`
|
||
RuntimeStaleSecs float64 `json:"runtime_stale_secs"`
|
||
}
|
||
|
||
// Re-delivers a task whose previous claim likely succeeded server-side but
|
||
// whose response never reached the daemon. The task is still in `dispatched`
|
||
// with no `started_at`, so the daemon has not acknowledged it via StartTask.
|
||
// Refresh dispatched_at so the server-side dispatch timeout measures from the
|
||
// recovered delivery attempt.
|
||
func (q *Queries) ReclaimStaleDispatchedTaskForRuntime(ctx context.Context, arg ReclaimStaleDispatchedTaskForRuntimeParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, reclaimStaleDispatchedTaskForRuntime,
|
||
arg.RuntimeID,
|
||
arg.PrepareLeaseSecs,
|
||
arg.ClaimRecoverySecs,
|
||
arg.RuntimeStaleSecs,
|
||
)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const reclaimStaleDispatchedTasksForRuntimes = `-- name: ReclaimStaleDispatchedTasksForRuntimes :many
|
||
UPDATE agent_task_queue
|
||
SET dispatched_at = now(),
|
||
prepare_lease_expires_at = now() + make_interval(secs => $1::double precision)
|
||
WHERE id IN (
|
||
SELECT atq.id FROM agent_task_queue atq
|
||
WHERE atq.runtime_id = ANY($2::uuid[])
|
||
AND atq.status = 'dispatched'
|
||
AND atq.started_at IS NULL
|
||
AND atq.dispatched_at < now() - make_interval(secs => $3::double precision)
|
||
AND (atq.prepare_lease_expires_at IS NULL OR atq.prepare_lease_expires_at < now())
|
||
AND EXISTS (
|
||
-- Keep the dispatched-reclaim owner fence intentionally stricter
|
||
-- than the queued claim carve-out below.
|
||
SELECT 1
|
||
FROM agent a
|
||
JOIN agent_runtime r ON r.id = atq.runtime_id
|
||
WHERE a.id = atq.agent_id
|
||
AND a.runtime_id = atq.runtime_id
|
||
AND (
|
||
r.visibility = 'public'
|
||
OR (
|
||
r.visibility = 'private'
|
||
AND (
|
||
r.owner_id IS NULL
|
||
OR a.owner_id IS NULL
|
||
OR r.owner_id = a.owner_id
|
||
)
|
||
)
|
||
)
|
||
AND r.status = 'online'
|
||
AND COALESCE(r.last_seen_at, r.updated_at) >=
|
||
now() - make_interval(secs => $4::double precision)
|
||
)
|
||
ORDER BY atq.priority DESC, atq.dispatched_at ASC
|
||
LIMIT $5::int
|
||
FOR UPDATE SKIP LOCKED
|
||
)
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type ReclaimStaleDispatchedTasksForRuntimesParams struct {
|
||
PrepareLeaseSecs float64 `json:"prepare_lease_secs"`
|
||
RuntimeIds []pgtype.UUID `json:"runtime_ids"`
|
||
ClaimRecoverySecs float64 `json:"claim_recovery_secs"`
|
||
RuntimeStaleSecs float64 `json:"runtime_stale_secs"`
|
||
MaxTasks int32 `json:"max_tasks"`
|
||
}
|
||
|
||
// Batch variant of ReclaimStaleDispatchedTaskForRuntime (MUL-4257): re-delivers
|
||
// up to @max_tasks tasks across the whole runtime set in one round trip, so a
|
||
// machine-level batch claim recovers lost-response dispatches for every runtime
|
||
// it hosts without one query per runtime. Same eligibility as the singular
|
||
// query (dispatched, never started, past the recovery window, expired/absent
|
||
// prepare lease) and the same dispatched_at refresh; only the runtime filter
|
||
// (= ANY) and the LIMIT (max_tasks instead of 1) differ.
|
||
func (q *Queries) ReclaimStaleDispatchedTasksForRuntimes(ctx context.Context, arg ReclaimStaleDispatchedTasksForRuntimesParams) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, reclaimStaleDispatchedTasksForRuntimes,
|
||
arg.PrepareLeaseSecs,
|
||
arg.RuntimeIds,
|
||
arg.ClaimRecoverySecs,
|
||
arg.RuntimeStaleSecs,
|
||
arg.MaxTasks,
|
||
)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const recoverOrphanedTasksForRuntime = `-- name: RecoverOrphanedTasksForRuntime :many
|
||
UPDATE agent_task_queue
|
||
SET status = 'failed',
|
||
completed_at = now(),
|
||
error = 'daemon restarted while task was in flight',
|
||
failure_reason = 'runtime_recovery',
|
||
wait_reason = NULL,
|
||
prepare_lease_expires_at = NULL
|
||
WHERE runtime_id = $1 AND status IN ('dispatched', 'running', 'waiting_local_directory')
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
// Called by the daemon at startup. Atomically fails any dispatched/running/
|
||
// waiting_local_directory task that the prior incarnation of this runtime
|
||
// owned but did not finalize. Returns the failed rows so callers can hand
|
||
// them to the auto-retry path. waiting_local_directory rows are included
|
||
// because the daemon holding the path lock is the same process that just
|
||
// died — without us, the row would sit waiting forever.
|
||
func (q *Queries) RecoverOrphanedTasksForRuntime(ctx context.Context, runtimeID pgtype.UUID) ([]AgentTaskQueue, error) {
|
||
rows, err := q.db.Query(ctx, recoverOrphanedTasksForRuntime, runtimeID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []AgentTaskQueue{}
|
||
for rows.Next() {
|
||
var i AgentTaskQueue
|
||
if err := rows.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, i)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
return items, nil
|
||
}
|
||
|
||
const refreshAgentStatusFromTasks = `-- name: RefreshAgentStatusFromTasks :one
|
||
WITH desired AS (
|
||
SELECT CASE WHEN EXISTS (
|
||
SELECT 1 FROM agent_task_queue q
|
||
WHERE q.agent_id = $1 AND q.status IN ('dispatched', 'running')
|
||
) THEN 'working' ELSE 'idle' END AS status
|
||
)
|
||
UPDATE agent AS a
|
||
SET status = desired.status,
|
||
updated_at = now()
|
||
FROM desired
|
||
WHERE a.id = $1 AND a.status IS DISTINCT FROM desired.status
|
||
RETURNING a.id, a.workspace_id, a.name, a.avatar_url, a.runtime_mode, a.runtime_config, a.visibility, a.status, a.max_concurrent_tasks, a.owner_id, a.created_at, a.updated_at, a.description, a.runtime_id, a.instructions, a.archived_at, a.archived_by, a.custom_env, a.custom_args, a.mcp_config, a.model, a.thinking_level, a.composio_toolkit_allowlist, a.permission_mode, a.kind, a.system_key, a.disabled_runtime_skills, a.service_tier, a.conversation_starters, a.connector_service_allowlist
|
||
`
|
||
|
||
// Persisted agent.status has no queued/resource-wait bucket. Keep dispatched
|
||
// as working because the daemon is actively preparing that task, but do not
|
||
// let a task parked on a local_directory mutex masquerade as executing work.
|
||
// The status guard makes a correct status a no-op: :one returns no rows, so
|
||
// callers neither rewrite updated_at nor publish a redundant status event.
|
||
func (q *Queries) RefreshAgentStatusFromTasks(ctx context.Context, id pgtype.UUID) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, refreshAgentStatusFromTasks, id)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const registerPlannedCommentForActiveTask = `-- name: RegisterPlannedCommentForActiveTask :one
|
||
UPDATE agent_task_queue
|
||
SET coalesced_comment_ids = (
|
||
SELECT COALESCE(array_agg(DISTINCT e), '{}')
|
||
FROM unnest(array_append(coalesced_comment_ids, $1::uuid)) AS e
|
||
WHERE e IS NOT NULL
|
||
)
|
||
WHERE id = (
|
||
SELECT t.id FROM agent_task_queue t
|
||
WHERE t.context->>'wakeup_id' IS NULL AND t.issue_id = $2
|
||
AND t.agent_id = $3
|
||
AND t.comment_thread_id IS NOT DISTINCT FROM comment_thread_root_id($1::uuid)
|
||
AND t.status IN ('dispatched', 'running', 'waiting_local_directory')
|
||
AND (
|
||
COALESCE($4::text, '') = ''
|
||
OR t.context->>'head_sha' = $4::text
|
||
)
|
||
ORDER BY t.created_at DESC
|
||
LIMIT 1
|
||
)
|
||
AND status IN ('dispatched', 'running', 'waiting_local_directory')
|
||
RETURNING id, coalesced_comment_ids
|
||
`
|
||
|
||
type RegisterPlannedCommentForActiveTaskParams struct {
|
||
CommentID pgtype.UUID `json:"comment_id"`
|
||
IssueID pgtype.UUID `json:"issue_id"`
|
||
AgentID pgtype.UUID `json:"agent_id"`
|
||
HeadSha pgtype.Text `json:"head_sha"`
|
||
}
|
||
|
||
type RegisterPlannedCommentForActiveTaskRow struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
CoalescedCommentIds []pgtype.UUID `json:"coalesced_comment_ids"`
|
||
}
|
||
|
||
// #5914: durably register a comment that lost an enqueue race as a PLANNED
|
||
// (undelivered) input on the same-(issue, agent) ACTIVE task whose queued row
|
||
// MergeCommentIntoPendingTask could no longer target (it was claimed →
|
||
// dispatched/running). The losing comment can predate that task's created_at,
|
||
// so completion reconciliation's `created_at > since` window cannot see it;
|
||
// appending it to coalesced_comment_ids (the planned set) WITHOUT touching
|
||
// delivered_comment_ids makes reconcileCommentsOnCompletion replay it as a
|
||
// single bounded follow-up (planned-but-not-delivered ⇒ follow-up).
|
||
//
|
||
// Head-scoped (TEN-356): only a task stamped with the SAME head_sha is a target,
|
||
// so a new-HEAD comment is never attached to an old-HEAD run. Returns
|
||
// pgx.ErrNoRows when no same-head active task exists (different HEAD, or the task
|
||
// just terminated) so the caller falls back to the active-task decision.
|
||
//
|
||
// 'queued' is DELIBERATELY EXCLUDED (#5914, Elon round 3): a not-yet-claimed
|
||
// task has no claim receipt, so a comment merged into it WILL be recorded as
|
||
// delivered at claim time and never earns a completion follow-up under its own
|
||
// attribution. A queued target must therefore go through the ATOMIC
|
||
// MergeCommentIntoPendingTask (which re-stamps trigger/originator/accountable/
|
||
// overlay), never a bare planned append — otherwise a second member's comment
|
||
// could execute under the first member's identity/connected-apps (MUL-4302).
|
||
// Only claim-receipt statuses (already-built delivered set) are safe planned-id
|
||
// targets.
|
||
// Recheck status on the UPDATE target after a concurrent row-lock wait. The
|
||
// subquery can see an active snapshot while completion commits; appending to
|
||
// that completed row would be too late for its completion replay to see it.
|
||
func (q *Queries) RegisterPlannedCommentForActiveTask(ctx context.Context, arg RegisterPlannedCommentForActiveTaskParams) (RegisterPlannedCommentForActiveTaskRow, error) {
|
||
row := q.db.QueryRow(ctx, registerPlannedCommentForActiveTask,
|
||
arg.CommentID,
|
||
arg.IssueID,
|
||
arg.AgentID,
|
||
arg.HeadSha,
|
||
)
|
||
var i RegisterPlannedCommentForActiveTaskRow
|
||
err := row.Scan(&i.ID, &i.CoalescedCommentIds)
|
||
return i, err
|
||
}
|
||
|
||
const requeueAgentTaskAfterClaimFailure = `-- name: RequeueAgentTaskAfterClaimFailure :one
|
||
UPDATE agent_task_queue
|
||
SET status = 'queued',
|
||
dispatched_at = NULL,
|
||
prepare_lease_expires_at = NULL,
|
||
delivered_comment_ids = '{}'
|
||
WHERE id = $1
|
||
AND runtime_id = $2
|
||
AND status = 'dispatched'
|
||
AND started_at IS NULL
|
||
AND dispatched_at = $3
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
type RequeueAgentTaskAfterClaimFailureParams struct {
|
||
TaskID pgtype.UUID `json:"task_id"`
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
DispatchedAt pgtype.Timestamptz `json:"dispatched_at"`
|
||
}
|
||
|
||
// Claim finalization (task token + optional comment receipt) failed before any
|
||
// response bytes were written. Return only that exact claim generation to the
|
||
// queue so another poll can retry immediately instead of waiting for stale
|
||
// dispatch recovery. The dispatched_at CAS prevents an old handler from
|
||
// rolling back a newer reclaim.
|
||
func (q *Queries) RequeueAgentTaskAfterClaimFailure(ctx context.Context, arg RequeueAgentTaskAfterClaimFailureParams) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, requeueAgentTaskAfterClaimFailure, arg.TaskID, arg.RuntimeID, arg.DispatchedAt)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const restoreAgent = `-- name: RestoreAgent :one
|
||
UPDATE agent SET archived_at = NULL, archived_by = NULL, updated_at = now()
|
||
WHERE id = $1
|
||
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist
|
||
`
|
||
|
||
func (q *Queries) RestoreAgent(ctx context.Context, id pgtype.UUID) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, restoreAgent, id)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const setAgentTaskBranchName = `-- name: SetAgentTaskBranchName :exec
|
||
UPDATE agent_task_queue
|
||
SET branch_name = COALESCE(branch_name, $1)
|
||
WHERE id = $2 AND status = 'cancelled'
|
||
`
|
||
|
||
type SetAgentTaskBranchNameParams struct {
|
||
BranchName pgtype.Text `json:"branch_name"`
|
||
ID pgtype.UUID `json:"id"`
|
||
}
|
||
|
||
// Records the delivered branch on a CANCELLED task. Needed because the daemon
|
||
// finalizes its worktree (committing whatever the agent produced) BEFORE it
|
||
// learns the task was cancelled, so the branch exists but the cancel path has
|
||
// no result payload to carry it. Without this the branch is real and
|
||
// completely undiscoverable from the UI.
|
||
//
|
||
// Never overwrites a name already recorded by a complete/fail callback, and
|
||
// never touches any OTHER terminal state: the daemon acks on every terminal
|
||
// status it observes, so a late or replayed ack from a stale run could
|
||
// otherwise write its branch into a completed/failed row whose own callback —
|
||
// the authoritative channel for those states — recorded nothing. status is
|
||
// stable once terminal, so this CAS cannot race a legitimate write.
|
||
func (q *Queries) SetAgentTaskBranchName(ctx context.Context, arg SetAgentTaskBranchNameParams) error {
|
||
_, err := q.db.Exec(ctx, setAgentTaskBranchName, arg.BranchName, arg.ID)
|
||
return err
|
||
}
|
||
|
||
const setAgentTaskDurableWorkDir = `-- name: SetAgentTaskDurableWorkDir :exec
|
||
UPDATE agent_task_queue
|
||
SET durable_work_dir = COALESCE(durable_work_dir, $1)
|
||
WHERE id = $2 AND status = 'cancelled'
|
||
`
|
||
|
||
type SetAgentTaskDurableWorkDirParams struct {
|
||
DurableWorkDir pgtype.Text `json:"durable_work_dir"`
|
||
ID pgtype.UUID `json:"id"`
|
||
}
|
||
|
||
// Records the durable replacement for a disposable worktree on a CANCELLED
|
||
// task. The daemon only sends this after Finalize confirms the task worktree
|
||
// is gone. As with branch_name, the status CAS rejects stale acknowledgements
|
||
// and COALESCE makes replays idempotent.
|
||
func (q *Queries) SetAgentTaskDurableWorkDir(ctx context.Context, arg SetAgentTaskDurableWorkDirParams) error {
|
||
_, err := q.db.Exec(ctx, setAgentTaskDurableWorkDir, arg.DurableWorkDir, arg.ID)
|
||
return err
|
||
}
|
||
|
||
const setAgentTaskErrorIfEmpty = `-- name: SetAgentTaskErrorIfEmpty :exec
|
||
UPDATE agent_task_queue
|
||
SET error = $1,
|
||
failure_reason = COALESCE(failure_reason, $2)
|
||
WHERE id = $3 AND (error IS NULL OR error = '') AND status = 'cancelled'
|
||
`
|
||
|
||
type SetAgentTaskErrorIfEmptyParams struct {
|
||
Error pgtype.Text `json:"error"`
|
||
FailureReason pgtype.Text `json:"failure_reason"`
|
||
ID pgtype.UUID `json:"id"`
|
||
}
|
||
|
||
// Companion to SetAgentTaskBranchName for the cancel-ack path. A cancelled
|
||
// worktree task whose Finalize ABORTED has no branch to deliver — the error
|
||
// text carrying the preserved-worktree path is the only pointer to the agent's
|
||
// work, and the cancel flow discarded the rest of the result. A user-initiated
|
||
// cancel leaves error NULL, so filling it here never overwrites a reason
|
||
// recorded by a fail callback or the claim gate; the status CAS keeps a late
|
||
// ack from stamping an error onto a completed/failed row (see
|
||
// SetAgentTaskBranchName above).
|
||
func (q *Queries) SetAgentTaskErrorIfEmpty(ctx context.Context, arg SetAgentTaskErrorIfEmptyParams) error {
|
||
_, err := q.db.Exec(ctx, setAgentTaskErrorIfEmpty, arg.Error, arg.FailureReason, arg.ID)
|
||
return err
|
||
}
|
||
|
||
const setDeferredChannelIssueTaskRuntimeOverlay = `-- name: SetDeferredChannelIssueTaskRuntimeOverlay :execrows
|
||
UPDATE agent_task_queue
|
||
SET runtime_mcp_overlay = $1,
|
||
runtime_connected_apps = $2,
|
||
runtime_connector_handles = $3
|
||
WHERE id = $4
|
||
AND status = 'deferred'
|
||
AND context->>'channel_issue_media_pending' = 'true'
|
||
AND trigger_comment_id IS NULL
|
||
AND originator_user_id IS NOT DISTINCT FROM $5::uuid
|
||
`
|
||
|
||
type SetDeferredChannelIssueTaskRuntimeOverlayParams struct {
|
||
RuntimeMcpOverlay []byte `json:"runtime_mcp_overlay"`
|
||
RuntimeConnectedApps []byte `json:"runtime_connected_apps"`
|
||
RuntimeConnectorHandles []byte `json:"runtime_connector_handles"`
|
||
ID pgtype.UUID `json:"id"`
|
||
ExpectedOriginatorUserID pgtype.UUID `json:"expected_originator_user_id"`
|
||
}
|
||
|
||
// The issue and its inert media-gated task commit atomically before the optional
|
||
// external overlay is built. Only hydrate the untouched original plan: a comment
|
||
// merge may already have replaced the trigger, attribution, and matching overlay.
|
||
func (q *Queries) SetDeferredChannelIssueTaskRuntimeOverlay(ctx context.Context, arg SetDeferredChannelIssueTaskRuntimeOverlayParams) (int64, error) {
|
||
result, err := q.db.Exec(ctx, setDeferredChannelIssueTaskRuntimeOverlay,
|
||
arg.RuntimeMcpOverlay,
|
||
arg.RuntimeConnectedApps,
|
||
arg.RuntimeConnectorHandles,
|
||
arg.ID,
|
||
arg.ExpectedOriginatorUserID,
|
||
)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return result.RowsAffected(), nil
|
||
}
|
||
|
||
const setTaskDeliveredCommentIDs = `-- name: SetTaskDeliveredCommentIDs :one
|
||
UPDATE agent_task_queue
|
||
SET delivered_comment_ids = $1::uuid[]
|
||
WHERE id = $2
|
||
AND runtime_id = $3
|
||
AND status = 'dispatched'
|
||
AND started_at IS NULL
|
||
AND dispatched_at = $4
|
||
AND trigger_comment_id IS NOT DISTINCT FROM $5::uuid
|
||
AND NOT EXISTS (
|
||
SELECT 1
|
||
FROM unnest($1::uuid[]) AS delivered(id)
|
||
WHERE delivered.id IS NULL
|
||
OR (
|
||
delivered.id IS DISTINCT FROM trigger_comment_id
|
||
AND NOT (delivered.id = ANY(coalesced_comment_ids))
|
||
)
|
||
)
|
||
RETURNING delivered_comment_ids
|
||
`
|
||
|
||
type SetTaskDeliveredCommentIDsParams struct {
|
||
DeliveredCommentIds []pgtype.UUID `json:"delivered_comment_ids"`
|
||
TaskID pgtype.UUID `json:"task_id"`
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
DispatchedAt pgtype.Timestamptz `json:"dispatched_at"`
|
||
ExpectedTriggerCommentID pgtype.UUID `json:"expected_trigger_comment_id"`
|
||
}
|
||
|
||
// Replace (rather than append to) the delivery receipt for this claim. A stale
|
||
// dispatched task may be reclaimed by a daemon with different capabilities,
|
||
// so only the ids embedded in the newest response count as delivered. The CAS
|
||
// keeps a stale handler from writing after execution starts, and the subset
|
||
// guard prevents acknowledging an id outside the task's enqueue-time plan.
|
||
func (q *Queries) SetTaskDeliveredCommentIDs(ctx context.Context, arg SetTaskDeliveredCommentIDsParams) ([]pgtype.UUID, error) {
|
||
row := q.db.QueryRow(ctx, setTaskDeliveredCommentIDs,
|
||
arg.DeliveredCommentIds,
|
||
arg.TaskID,
|
||
arg.RuntimeID,
|
||
arg.DispatchedAt,
|
||
arg.ExpectedTriggerCommentID,
|
||
)
|
||
var delivered_comment_ids []pgtype.UUID
|
||
err := row.Scan(&delivered_comment_ids)
|
||
return delivered_comment_ids, err
|
||
}
|
||
|
||
const setTaskIssueSnapshot = `-- name: SetTaskIssueSnapshot :exec
|
||
UPDATE agent_task_queue
|
||
SET issue_snapshot = $1
|
||
WHERE id = $2
|
||
AND runtime_id = $3
|
||
AND status = 'dispatched'
|
||
AND started_at IS NULL
|
||
AND dispatched_at = $4
|
||
`
|
||
|
||
type SetTaskIssueSnapshotParams struct {
|
||
IssueSnapshot []byte `json:"issue_snapshot"`
|
||
TaskID pgtype.UUID `json:"task_id"`
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
DispatchedAt pgtype.Timestamptz `json:"dispatched_at"`
|
||
}
|
||
|
||
// Record the comparable issue state this claim's payload was built from, so the
|
||
// NEXT run this agent takes on the issue can be told whether the issue itself
|
||
// moved. Written for every issue-bound claim, not just comment-backed ones: an
|
||
// assignment run that skips this leaves the following run with no baseline to
|
||
// compare against, which reads as "not compared" and costs an extra issue read.
|
||
// Same CAS as SetTaskDeliveredCommentIDs so a stale handler cannot overwrite a
|
||
// newer reclaim's snapshot, or write one after execution has started.
|
||
func (q *Queries) SetTaskIssueSnapshot(ctx context.Context, arg SetTaskIssueSnapshotParams) error {
|
||
_, err := q.db.Exec(ctx, setTaskIssueSnapshot,
|
||
arg.IssueSnapshot,
|
||
arg.TaskID,
|
||
arg.RuntimeID,
|
||
arg.DispatchedAt,
|
||
)
|
||
return err
|
||
}
|
||
|
||
const setTaskWorkflowStep = `-- name: SetTaskWorkflowStep :exec
|
||
UPDATE agent_task_queue SET workflow_step = $1 WHERE id = $2
|
||
`
|
||
|
||
type SetTaskWorkflowStepParams struct {
|
||
WorkflowStep pgtype.Text `json:"workflow_step"`
|
||
ID pgtype.UUID `json:"id"`
|
||
}
|
||
|
||
// Moves a run's workflow step along with a status change the run itself made
|
||
// (MUL-7420). See migration 557.
|
||
func (q *Queries) SetTaskWorkflowStep(ctx context.Context, arg SetTaskWorkflowStepParams) error {
|
||
_, err := q.db.Exec(ctx, setTaskWorkflowStep, arg.WorkflowStep, arg.ID)
|
||
return err
|
||
}
|
||
|
||
const settleDelegatedFailureRecoveriesForTask = `-- name: SettleDelegatedFailureRecoveriesForTask :execrows
|
||
UPDATE comment recovery
|
||
SET recovery_settled_at = now()
|
||
FROM agent_task_queue task
|
||
WHERE task.id = $1
|
||
AND task.status IN ('completed', 'failed', 'cancelled')
|
||
AND recovery.id = ANY(task.delivered_comment_ids)
|
||
AND recovery.author_type = 'system'
|
||
AND recovery.type = 'progress_update'
|
||
AND recovery.source_task_id IS NOT NULL
|
||
AND recovery.recovery_settled_at IS NULL
|
||
`
|
||
|
||
// Retire every delegated-failure recovery comment this task actually received.
|
||
//
|
||
// Callers MUST run this inside the same transaction that makes the task
|
||
// terminal, and only there. While a task is still dispatched its receipt is
|
||
// REPLACED rather than appended (SetTaskDeliveredCommentIDs), because a reclaim
|
||
// by a differently-capable daemon must be able to drop an id it will not
|
||
// deliver. Settling at receipt-write time would freeze that legitimate
|
||
// transient window into a permanently lost recovery. Once the task is terminal
|
||
// the receipt is final, which is exactly the guarantee this marker records.
|
||
//
|
||
// A comment that was only planned into the task and never delivered is absent
|
||
// from delivered_comment_ids, so an automatic cancellation correctly leaves it
|
||
// pending for the sweeper to replay.
|
||
//
|
||
// The terminal-status predicate is the guarantee itself, not a caller
|
||
// convention. Settling a dispatched or running task's receipt would freeze the
|
||
// reclaim window into a permanently lost recovery, and this marker is
|
||
// monotonic — there is no later pass that could undo it. A caller that passes a
|
||
// non-terminal task therefore updates nothing rather than silently destroying
|
||
// the obligation.
|
||
func (q *Queries) SettleDelegatedFailureRecoveriesForTask(ctx context.Context, taskID pgtype.UUID) (int64, error) {
|
||
result, err := q.db.Exec(ctx, settleDelegatedFailureRecoveriesForTask, taskID)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return result.RowsAffected(), nil
|
||
}
|
||
|
||
const settleDelegatedFailureRecoveryComment = `-- name: SettleDelegatedFailureRecoveryComment :execrows
|
||
UPDATE comment
|
||
SET recovery_settled_at = now()
|
||
WHERE id = $1
|
||
AND author_type = 'system'
|
||
AND type = 'progress_update'
|
||
AND source_task_id IS NOT NULL
|
||
AND recovery_settled_at IS NULL
|
||
`
|
||
|
||
// Retire one recovery comment by id, for the terminal outcomes that are not a
|
||
// task reaching a terminal status: exhaustion of the bounded automatic attempts
|
||
// writes its receipt onto an attempt row that may still be running, and the
|
||
// user-visible explanation comment is what closes the obligation. Callers run
|
||
// this in the same transaction that records that outcome.
|
||
func (q *Queries) SettleDelegatedFailureRecoveryComment(ctx context.Context, commentID pgtype.UUID) (int64, error) {
|
||
result, err := q.db.Exec(ctx, settleDelegatedFailureRecoveryComment, commentID)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return result.RowsAffected(), nil
|
||
}
|
||
|
||
const startAgentTask = `-- name: StartAgentTask :one
|
||
UPDATE agent_task_queue
|
||
SET status = 'running',
|
||
started_at = now(),
|
||
wait_reason = NULL,
|
||
prepare_lease_expires_at = NULL
|
||
WHERE agent_task_queue.id = $1 AND agent_task_queue.status IN ('dispatched', 'waiting_local_directory')
|
||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot, workflow_step, runtime_connector_handles
|
||
`
|
||
|
||
// Legacy single-winner transition; generation-aware callers lock the claim first.
|
||
// Transitions a task to running. Accepts either 'dispatched' (the normal
|
||
// claim → run flow) or 'waiting_local_directory' (the daemon held the row in
|
||
// a wait state while another task owned the local_directory path lock; once
|
||
// the lock was acquired the daemon flips here). wait_reason is cleared on
|
||
// the transition so a future read can't conflate "currently waiting" with
|
||
// "previously waited".
|
||
func (q *Queries) StartAgentTask(ctx context.Context, id pgtype.UUID) (AgentTaskQueue, error) {
|
||
row := q.db.QueryRow(ctx, startAgentTask, id)
|
||
var i AgentTaskQueue
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.AgentID,
|
||
&i.IssueID,
|
||
&i.Status,
|
||
&i.Priority,
|
||
&i.DispatchedAt,
|
||
&i.StartedAt,
|
||
&i.CompletedAt,
|
||
&i.Result,
|
||
&i.Error,
|
||
&i.CreatedAt,
|
||
&i.Context,
|
||
&i.RuntimeID,
|
||
&i.SessionID,
|
||
&i.WorkDir,
|
||
&i.TriggerCommentID,
|
||
&i.ChatSessionID,
|
||
&i.AutopilotRunID,
|
||
&i.Attempt,
|
||
&i.MaxAttempts,
|
||
&i.ParentTaskID,
|
||
&i.FailureReason,
|
||
&i.TriggerSummary,
|
||
&i.ForceFreshSession,
|
||
&i.IsLeaderTask,
|
||
&i.WaitReason,
|
||
&i.InitiatorUserID,
|
||
&i.HandoffNote,
|
||
&i.PrepareLeaseExpiresAt,
|
||
&i.SquadID,
|
||
&i.RuntimeMcpOverlay,
|
||
&i.EscalationForTaskID,
|
||
&i.FireAt,
|
||
&i.OriginatorUserID,
|
||
&i.RuntimeConnectedApps,
|
||
&i.CoalescedCommentIds,
|
||
&i.DeliveredCommentIds,
|
||
&i.ChatInputTaskID,
|
||
&i.ChatFinalizeDeferredAt,
|
||
&i.OriginatorSource,
|
||
&i.DelegatedFromTaskID,
|
||
&i.RetryOfTaskID,
|
||
&i.RerunOfTaskID,
|
||
&i.RuleVersionID,
|
||
&i.TriggerEvidenceKind,
|
||
&i.TriggerEvidenceRefID,
|
||
&i.AccountableUserID,
|
||
&i.SessionRolloutMissing,
|
||
&i.RetiredSessionID,
|
||
&i.QuickActionsDisabled,
|
||
&i.RegenerateQuickActionsFor,
|
||
&i.BranchName,
|
||
&i.DurableWorkDir,
|
||
&i.ChannelContextRevision,
|
||
&i.CommentThreadID,
|
||
&i.CancelledByType,
|
||
&i.CancelledByID,
|
||
&i.CancelledByName,
|
||
&i.IssueSnapshot,
|
||
&i.WorkflowStep,
|
||
&i.RuntimeConnectorHandles,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const updateAgent = `-- name: UpdateAgent :one
|
||
UPDATE agent SET
|
||
name = COALESCE($2, name),
|
||
description = COALESCE($3, description),
|
||
avatar_url = COALESCE($4, avatar_url),
|
||
runtime_config = COALESCE($5, runtime_config),
|
||
runtime_mode = COALESCE($6, runtime_mode),
|
||
runtime_id = COALESCE($7, runtime_id),
|
||
visibility = COALESCE($8, visibility),
|
||
permission_mode = COALESCE($9, permission_mode),
|
||
status = COALESCE($10, status),
|
||
max_concurrent_tasks = COALESCE($11, max_concurrent_tasks),
|
||
instructions = COALESCE($12, instructions),
|
||
custom_env = COALESCE($13, custom_env),
|
||
custom_args = COALESCE($14, custom_args),
|
||
mcp_config = COALESCE($15, mcp_config),
|
||
model = COALESCE($16, model),
|
||
thinking_level = COALESCE($17, thinking_level),
|
||
service_tier = COALESCE($18, service_tier),
|
||
conversation_starters = COALESCE($19, conversation_starters),
|
||
composio_toolkit_allowlist = COALESCE($20::text[], composio_toolkit_allowlist),
|
||
connector_service_allowlist = COALESCE($21::text[], connector_service_allowlist),
|
||
updated_at = now()
|
||
WHERE id = $1
|
||
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist
|
||
`
|
||
|
||
type UpdateAgentParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
Name pgtype.Text `json:"name"`
|
||
Description pgtype.Text `json:"description"`
|
||
AvatarUrl pgtype.Text `json:"avatar_url"`
|
||
RuntimeConfig []byte `json:"runtime_config"`
|
||
RuntimeMode pgtype.Text `json:"runtime_mode"`
|
||
RuntimeID pgtype.UUID `json:"runtime_id"`
|
||
Visibility pgtype.Text `json:"visibility"`
|
||
PermissionMode pgtype.Text `json:"permission_mode"`
|
||
Status pgtype.Text `json:"status"`
|
||
MaxConcurrentTasks pgtype.Int4 `json:"max_concurrent_tasks"`
|
||
Instructions pgtype.Text `json:"instructions"`
|
||
CustomEnv []byte `json:"custom_env"`
|
||
CustomArgs []byte `json:"custom_args"`
|
||
McpConfig []byte `json:"mcp_config"`
|
||
Model pgtype.Text `json:"model"`
|
||
ThinkingLevel pgtype.Text `json:"thinking_level"`
|
||
ServiceTier pgtype.Text `json:"service_tier"`
|
||
ConversationStarters []byte `json:"conversation_starters"`
|
||
ComposioToolkitAllowlist []string `json:"composio_toolkit_allowlist"`
|
||
ConnectorServiceAllowlist []string `json:"connector_service_allowlist"`
|
||
}
|
||
|
||
// composio_toolkit_allowlist is set wholesale: the API layer is responsible
|
||
// for normalising the request payload to either (a) the new slug list — sent
|
||
// here verbatim — or (b) an empty array to explicitly disable Composio.
|
||
// Distinguish "field omitted" (preserve) from "explicit clear" via
|
||
// ClearAgentComposioToolkitAllowlist below, mirroring the
|
||
// thinking_level / mcp_config two-query pattern: COALESCE can't restore NULL.
|
||
func (q *Queries) UpdateAgent(ctx context.Context, arg UpdateAgentParams) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, updateAgent,
|
||
arg.ID,
|
||
arg.Name,
|
||
arg.Description,
|
||
arg.AvatarUrl,
|
||
arg.RuntimeConfig,
|
||
arg.RuntimeMode,
|
||
arg.RuntimeID,
|
||
arg.Visibility,
|
||
arg.PermissionMode,
|
||
arg.Status,
|
||
arg.MaxConcurrentTasks,
|
||
arg.Instructions,
|
||
arg.CustomEnv,
|
||
arg.CustomArgs,
|
||
arg.McpConfig,
|
||
arg.Model,
|
||
arg.ThinkingLevel,
|
||
arg.ServiceTier,
|
||
arg.ConversationStarters,
|
||
arg.ComposioToolkitAllowlist,
|
||
arg.ConnectorServiceAllowlist,
|
||
)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const updateAgentCustomEnv = `-- name: UpdateAgentCustomEnv :one
|
||
UPDATE agent
|
||
SET custom_env = $2, updated_at = now()
|
||
WHERE id = $1
|
||
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist
|
||
`
|
||
|
||
type UpdateAgentCustomEnvParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
CustomEnv []byte `json:"custom_env"`
|
||
}
|
||
|
||
// Replaces an agent's custom_env map wholesale. Used by the dedicated
|
||
// env-management endpoint (POST/PUT /api/agents/{id}/env), which is the
|
||
// only post-creation write path for env. UpdateAgent has been stripped
|
||
// of custom_env handling so all env mutations flow through here and the
|
||
// handler's audit-log + **** sentinel guard.
|
||
func (q *Queries) UpdateAgentCustomEnv(ctx context.Context, arg UpdateAgentCustomEnvParams) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, updateAgentCustomEnv, arg.ID, arg.CustomEnv)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const updateAgentDisabledRuntimeSkills = `-- name: UpdateAgentDisabledRuntimeSkills :one
|
||
UPDATE agent
|
||
SET disabled_runtime_skills = $2, updated_at = now()
|
||
WHERE id = $1
|
||
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist
|
||
`
|
||
|
||
type UpdateAgentDisabledRuntimeSkillsParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
DisabledRuntimeSkills []byte `json:"disabled_runtime_skills"`
|
||
}
|
||
|
||
func (q *Queries) UpdateAgentDisabledRuntimeSkills(ctx context.Context, arg UpdateAgentDisabledRuntimeSkillsParams) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, updateAgentDisabledRuntimeSkills, arg.ID, arg.DisabledRuntimeSkills)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const updateAgentStatus = `-- name: UpdateAgentStatus :one
|
||
UPDATE agent SET status = $2, updated_at = now()
|
||
WHERE id = $1
|
||
RETURNING id, workspace_id, name, avatar_url, runtime_mode, runtime_config, visibility, status, max_concurrent_tasks, owner_id, created_at, updated_at, description, runtime_id, instructions, archived_at, archived_by, custom_env, custom_args, mcp_config, model, thinking_level, composio_toolkit_allowlist, permission_mode, kind, system_key, disabled_runtime_skills, service_tier, conversation_starters, connector_service_allowlist
|
||
`
|
||
|
||
type UpdateAgentStatusParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
Status string `json:"status"`
|
||
}
|
||
|
||
func (q *Queries) UpdateAgentStatus(ctx context.Context, arg UpdateAgentStatusParams) (Agent, error) {
|
||
row := q.db.QueryRow(ctx, updateAgentStatus, arg.ID, arg.Status)
|
||
var i Agent
|
||
err := row.Scan(
|
||
&i.ID,
|
||
&i.WorkspaceID,
|
||
&i.Name,
|
||
&i.AvatarUrl,
|
||
&i.RuntimeMode,
|
||
&i.RuntimeConfig,
|
||
&i.Visibility,
|
||
&i.Status,
|
||
&i.MaxConcurrentTasks,
|
||
&i.OwnerID,
|
||
&i.CreatedAt,
|
||
&i.UpdatedAt,
|
||
&i.Description,
|
||
&i.RuntimeID,
|
||
&i.Instructions,
|
||
&i.ArchivedAt,
|
||
&i.ArchivedBy,
|
||
&i.CustomEnv,
|
||
&i.CustomArgs,
|
||
&i.McpConfig,
|
||
&i.Model,
|
||
&i.ThinkingLevel,
|
||
&i.ComposioToolkitAllowlist,
|
||
&i.PermissionMode,
|
||
&i.Kind,
|
||
&i.SystemKey,
|
||
&i.DisabledRuntimeSkills,
|
||
&i.ServiceTier,
|
||
&i.ConversationStarters,
|
||
&i.ConnectorServiceAllowlist,
|
||
)
|
||
return i, err
|
||
}
|
||
|
||
const updateAgentTaskSession = `-- name: UpdateAgentTaskSession :exec
|
||
UPDATE agent_task_queue
|
||
SET session_id = COALESCE($2, session_id),
|
||
work_dir = COALESCE($3, work_dir)
|
||
WHERE id = $1
|
||
AND (
|
||
status IN ('dispatched', 'running')
|
||
OR (status = 'cancelled' AND session_id IS NULL)
|
||
)
|
||
`
|
||
|
||
type UpdateAgentTaskSessionParams struct {
|
||
ID pgtype.UUID `json:"id"`
|
||
SessionID pgtype.Text `json:"session_id"`
|
||
WorkDir pgtype.Text `json:"work_dir"`
|
||
}
|
||
|
||
// Pins the resume pointer mid-flight so a daemon crash leaves a usable
|
||
// session_id/work_dir on the task row. No-op if the task is no longer
|
||
// in dispatched/running. waiting_local_directory tasks have no session yet
|
||
// so this query intentionally skips them.
|
||
//
|
||
// A row the user just cancelled accepts the pin too, but only while its
|
||
// session slot is still empty (GH #6340). The pin is asynchronous — for Codex
|
||
// it waits for the rollout to reach the store — so a cancel landing in that
|
||
// window used to drop the session id for good, and the cancelled turn's
|
||
// context with it. Filling an EMPTY slot on the run's own row is exactly what
|
||
// the mid-flight pin is for; the `session_id IS NULL` guard is what keeps this
|
||
// from being an overwrite, and completed/failed rows stay untouchable so a
|
||
// straggler goroutine can never contradict a terminal report.
|
||
func (q *Queries) UpdateAgentTaskSession(ctx context.Context, arg UpdateAgentTaskSessionParams) error {
|
||
_, err := q.db.Exec(ctx, updateAgentTaskSession, arg.ID, arg.SessionID, arg.WorkDir)
|
||
return err
|
||
}
|