Files
multica/server/pkg/db/generated/agent.sql.go
Bohan Jiang 30318b79bc MUL-5426: fix(daemon): retire sessions whose history the provider refuses to replay (#6083)
* fix(daemon): retire sessions whose history the provider refuses to replay

A run killed mid-reply (machine shutdown, force-quit, SIGKILL) can leave an
empty assistant message in the agent CLI's transcript. Every later resume
replays it, the provider rejects the request, and the (agent, issue) pair is
bricked with no self-healing and no user-facing recovery.

Multica already has the mechanism for this — poisoned-session classification —
but its detector paired "400" with "invalid_request_error", which is the
Anthropic wire shape. The same defect reported by any other provider carried
neither token, so it classified as agent_error.unknown: resume-safe by
omission. GetLastTaskSession kept handing back the dead session on every
follow-up, manual Rerun resolved it through the same predicate, and the
in-turn fresh-session retry never fired because ResumeRejected is false here
(nothing rejected the resume — the transcript loaded and the provider refused
to replay it).

Add taskfailure.UnresumableHistory, which recognises the defect by what the
provider says is wrong — some content is empty, and here is which message in
the history — rather than by status code or provider name. Both signals are
required, so a tool reporting "field must not be empty" does not match.

Wire it into the four places that decide whether a session survives:

- classifyPoisonedError, so the task is written as api_invalid_request
- shouldRetryWithFreshSession, so the turn recovers on all 17 backends
  instead of the subset whose adapter learned to detect it; the tools == 0
  gate is unchanged, so a run that already used a tool is never re-run
- ResumeUnsafeFailure, covering the manual-Rerun path
- both resume queries, as defense-in-depth for hosts whose daemon predates
  this (self-host daemons upgrade on their own cadence)

Fixes #6066. Also covers the daemon half of #5760.

Co-authored-by: multica-agent <github@multica.ai>

* fix(session): close the Chat and fresh-retry paths that resurrect a poisoned session

Review found the previous commit stopped short in two places, both of which
put the dead transcript back in play.

Chat never consulted the guarded query. The claim handler reads
chat_session.session_id first and only falls back to GetLastChatTaskSession
when it is empty, so a poisoned pointer there bypasses every filter that query
applies. The fail path merely declined to OVERWRITE the pointer, leaving it in
place. It now clears it in the same transaction, matched on session and
runtime so a concurrent turn's newer pointer survives. The promote guard moves
to ResumeUnsafeFailure as well — the reason-only check passed an un-upgraded
daemon's agent_error.unknown row and re-pinned what the clear had just removed.

GetLastChatTaskSession also kept the row-level filter the issue query dropped
in GH #5975: it discarded the newest poisoned row and fell back to an older
completed row carrying the same dead session. It now judges each session by
its latest terminal state, matching GetLastTaskSession.

A recovered turn could not retire anything. A terminal report carried one
session_id, and an empty one meant both "nothing to report" and "forget the
old session", so a fresh-session retry that SUCCEEDED left the id it retried
away from selectable — through an older completed row on the issue, or through
the chat pointer. agent_task_queue.retired_session_id records the abandonment
itself, reported on every terminal path including completed, and both resume
lookups exclude it. This is the contract gap the previous PR deferred; the
fresh-retry path now runs on all backends, so deferring it is not safe.

Also narrows what the cross-backend test claims: it pins the shared decision,
not that all 17 adapters surface the error into Result.Error (#5760 is the
counter-example), and says so.

Co-authored-by: multica-agent <github@multica.ai>

* test(session): require pgx.ErrNoRows in the resume-exclusion assertions

The `if err == nil && prior.SessionID.Valid` form these tests shared is
false-green: any real fault — undefined column, syntax error, dead connection
— makes err non-nil, so the condition is false and the test passes. Run
against a database missing this branch's new column, the exclusion tests
reported PASS on a SQLSTATE 42703, meaning they could not have caught a broken
query.

requireSessionExcluded demands pgx.ErrNoRows specifically and fails loudly on
anything else, so a green run now means the filter worked rather than the
query never ran.

Applied to all nine sites, not just the four this branch added: the other five
guard the same GetLastTaskSession exclusion behaviour that this branch
changes, so leaving them false-green would leave the change under-tested. All
nine pass on a correctly migrated database.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-29 15:54:51 +08:00

5881 lines
214 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 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
`
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,
)
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
`
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,
); 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
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
`
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.
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,
); 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
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
`
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,
)
return i, err
}
const cancelAgentTasksByAgent = `-- name: CancelAgentTasksByAgent :many
UPDATE agent_task_queue
SET status = 'cancelled', completed_at = now(), prepare_lease_expires_at = 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
`
// 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 / CancelAgentTasksByIssueAndAgent
// (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,
); 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
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
`
// 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,
); 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
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
`
// 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,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const cancelAgentTasksByIssueAndAgent = `-- name: CancelAgentTasksByIssueAndAgent :many
UPDATE agent_task_queue
SET status = 'cancelled', completed_at = now(), prepare_lease_expires_at = NULL
WHERE issue_id = $1 AND agent_id = $2 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
`
type CancelAgentTasksByIssueAndAgentParams struct {
IssueID pgtype.UUID `json:"issue_id"`
AgentID pgtype.UUID `json:"agent_id"`
}
// Cancels active tasks for a single (issue, agent) pair without touching
// tasks belonging to other agents on the same issue. Used by the manual
// rerun flow so re-running the assignee doesn't collateral-cancel a
// still-running @-mention agent on the same issue.
func (q *Queries) CancelAgentTasksByIssueAndAgent(ctx context.Context, arg CancelAgentTasksByIssueAndAgentParams) ([]AgentTaskQueue, error) {
rows, err := q.db.Query(ctx, cancelAgentTasksByIssueAndAgent, 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,
); 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
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
`
// 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,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const cancelDeferredEscalationsForIssueAgent = `-- name: CancelDeferredEscalationsForIssueAgent :many
WITH cancelled AS (
UPDATE agent_task_queue fallback
SET status = 'cancelled', completed_at = now(), prepare_lease_expires_at = NULL
FROM agent_task_queue primary_task
WHERE fallback.escalation_for_task_id = primary_task.id
AND fallback.status IN ('deferred', 'queued', 'dispatched', 'waiting_local_directory')
AND primary_task.issue_id = $1
AND primary_task.agent_id = $2
RETURNING fallback.id, fallback.agent_id, fallback.issue_id, fallback.status, fallback.priority, fallback.dispatched_at, fallback.started_at, fallback.completed_at, fallback.result, fallback.error, fallback.created_at, fallback.context, fallback.runtime_id, fallback.session_id, fallback.work_dir, fallback.trigger_comment_id, fallback.chat_session_id, fallback.autopilot_run_id, fallback.attempt, fallback.max_attempts, fallback.parent_task_id, fallback.failure_reason, fallback.trigger_summary, fallback.force_fresh_session, fallback.is_leader_task, fallback.wait_reason, fallback.initiator_user_id, fallback.handoff_note, fallback.prepare_lease_expires_at, fallback.squad_id, fallback.runtime_mcp_overlay, fallback.escalation_for_task_id, fallback.fire_at, fallback.originator_user_id, fallback.runtime_connected_apps, fallback.coalesced_comment_ids, fallback.delivered_comment_ids, fallback.chat_input_task_id, fallback.chat_finalize_deferred_at, fallback.originator_source, fallback.delegated_from_task_id, fallback.retry_of_task_id, fallback.rerun_of_task_id, fallback.rule_version_id, fallback.trigger_evidence_kind, fallback.trigger_evidence_ref_id, fallback.accountable_user_id, fallback.session_rollout_missing, fallback.retired_session_id
)
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 FROM cancelled
`
type CancelDeferredEscalationsForIssueAgentParams struct {
IssueID pgtype.UUID `json:"issue_id"`
AgentID pgtype.UUID `json:"agent_id"`
}
type CancelDeferredEscalationsForIssueAgentRow struct {
ID pgtype.UUID `json:"id"`
AgentID pgtype.UUID `json:"agent_id"`
IssueID pgtype.UUID `json:"issue_id"`
Status string `json:"status"`
Priority int32 `json:"priority"`
DispatchedAt pgtype.Timestamptz `json:"dispatched_at"`
StartedAt pgtype.Timestamptz `json:"started_at"`
CompletedAt pgtype.Timestamptz `json:"completed_at"`
Result []byte `json:"result"`
Error pgtype.Text `json:"error"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
Context []byte `json:"context"`
RuntimeID pgtype.UUID `json:"runtime_id"`
SessionID pgtype.Text `json:"session_id"`
WorkDir pgtype.Text `json:"work_dir"`
TriggerCommentID pgtype.UUID `json:"trigger_comment_id"`
ChatSessionID pgtype.UUID `json:"chat_session_id"`
AutopilotRunID pgtype.UUID `json:"autopilot_run_id"`
Attempt int32 `json:"attempt"`
MaxAttempts int32 `json:"max_attempts"`
ParentTaskID pgtype.UUID `json:"parent_task_id"`
FailureReason pgtype.Text `json:"failure_reason"`
TriggerSummary pgtype.Text `json:"trigger_summary"`
ForceFreshSession bool `json:"force_fresh_session"`
IsLeaderTask bool `json:"is_leader_task"`
WaitReason pgtype.Text `json:"wait_reason"`
InitiatorUserID pgtype.UUID `json:"initiator_user_id"`
HandoffNote pgtype.Text `json:"handoff_note"`
PrepareLeaseExpiresAt pgtype.Timestamptz `json:"prepare_lease_expires_at"`
SquadID pgtype.UUID `json:"squad_id"`
RuntimeMcpOverlay []byte `json:"runtime_mcp_overlay"`
EscalationForTaskID pgtype.UUID `json:"escalation_for_task_id"`
FireAt pgtype.Timestamptz `json:"fire_at"`
OriginatorUserID pgtype.UUID `json:"originator_user_id"`
RuntimeConnectedApps []byte `json:"runtime_connected_apps"`
CoalescedCommentIds []pgtype.UUID `json:"coalesced_comment_ids"`
DeliveredCommentIds []pgtype.UUID `json:"delivered_comment_ids"`
ChatInputTaskID pgtype.UUID `json:"chat_input_task_id"`
ChatFinalizeDeferredAt pgtype.Timestamptz `json:"chat_finalize_deferred_at"`
OriginatorSource pgtype.Text `json:"originator_source"`
DelegatedFromTaskID pgtype.UUID `json:"delegated_from_task_id"`
RetryOfTaskID pgtype.UUID `json:"retry_of_task_id"`
RerunOfTaskID pgtype.UUID `json:"rerun_of_task_id"`
RuleVersionID pgtype.UUID `json:"rule_version_id"`
TriggerEvidenceKind pgtype.Text `json:"trigger_evidence_kind"`
TriggerEvidenceRefID pgtype.UUID `json:"trigger_evidence_ref_id"`
AccountableUserID pgtype.UUID `json:"accountable_user_id"`
SessionRolloutMissing bool `json:"session_rollout_missing"`
RetiredSessionID pgtype.Text `json:"retired_session_id"`
}
func (q *Queries) CancelDeferredEscalationsForIssueAgent(ctx context.Context, arg CancelDeferredEscalationsForIssueAgentParams) ([]CancelDeferredEscalationsForIssueAgentRow, error) {
rows, err := q.db.Query(ctx, cancelDeferredEscalationsForIssueAgent, arg.IssueID, arg.AgentID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []CancelDeferredEscalationsForIssueAgentRow{}
for rows.Next() {
var i CancelDeferredEscalationsForIssueAgentRow
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,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const cancelDeferredEscalationsForTask = `-- name: CancelDeferredEscalationsForTask :many
UPDATE agent_task_queue
SET status = 'cancelled', completed_at = now(), prepare_lease_expires_at = NULL
WHERE escalation_for_task_id = $1
AND status IN ('deferred', 'queued', '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
`
func (q *Queries) CancelDeferredEscalationsForTask(ctx context.Context, escalationForTaskID pgtype.UUID) ([]AgentTaskQueue, error) {
rows, err := q.db.Query(ctx, cancelDeferredEscalationsForTask, escalationForTaskID)
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,
); 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 => $2::double precision)
WHERE id = (
SELECT atq.id FROM agent_task_queue atq
WHERE atq.agent_id = $1 AND atq.status = 'queued'
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
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
`
type ClaimAgentTaskParams struct {
AgentID pgtype.UUID `json:"agent_id"`
PrepareLeaseSecs float64 `json:"prepare_lease_secs"`
}
// Claims the next queued task for an agent, 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.AgentID, 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,
)
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
`
// 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,
)
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
`
// 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,
)
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
`
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,
)
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
`
// 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,
)
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
`
// 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,
)
return i, 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,
session_rollout_missing = $5,
retired_session_id = COALESCE($6, 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
`
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"`
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.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,
)
return i, err
}
const countRunningTasks = `-- name: CountRunningTasks :one
SELECT count(*) FROM agent_task_queue
WHERE agent_id = $1 AND status IN ('dispatched', 'running', 'waiting_local_directory')
`
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,
composio_toolkit_allowlist, permission_mode
) VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8, $9, $10,
$11, $12, $13, $14, $15, $16,
$17,
$18::text[],
COALESCE($19, 'private')
)
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
`
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"`
ComposioToolkitAllowlist []string `json:"composio_toolkit_allowlist"`
PermissionMode interface{} `json:"permission_mode"`
}
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.ComposioToolkitAllowlist,
arg.PermissionMode,
)
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,
)
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
`
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,
)
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,
originator_source, delegated_from_task_id, rule_version_id, rerun_of_task_id, trigger_evidence_kind, trigger_evidence_ref_id
)
VALUES (
$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
)
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
`
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"`
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"`
}
// 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.
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.OriginatorSource,
arg.DelegatedFromTaskID,
arg.RuleVersionID,
arg.RerunOfTaskID,
arg.TriggerEvidenceKind,
arg.TriggerEvidenceRefID,
)
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,
)
return i, err
}
const createDeferredAgentTask = `-- name: CreateDeferredAgentTask :one
INSERT INTO agent_task_queue (
agent_id, runtime_id, issue_id, status, priority, trigger_comment_id,
trigger_summary, is_leader_task, squad_id, escalation_for_task_id, fire_at,
originator_user_id, accountable_user_id, originator_source,
delegated_from_task_id, trigger_evidence_kind, trigger_evidence_ref_id
)
VALUES (
$1, $2, $3, 'deferred', $4,
$5,
$6,
COALESCE($7::boolean, FALSE),
$8,
$9,
$10,
$11,
$12,
$13,
$14,
$15,
$16
)
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
`
type CreateDeferredAgentTaskParams 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"`
TriggerSummary pgtype.Text `json:"trigger_summary"`
IsLeaderTask pgtype.Bool `json:"is_leader_task"`
SquadID pgtype.UUID `json:"squad_id"`
EscalationForTaskID pgtype.UUID `json:"escalation_for_task_id"`
FireAt pgtype.Timestamptz `json:"fire_at"`
OriginatorUserID pgtype.UUID `json:"originator_user_id"`
AccountableUserID pgtype.UUID `json:"accountable_user_id"`
OriginatorSource pgtype.Text `json:"originator_source"`
DelegatedFromTaskID pgtype.UUID `json:"delegated_from_task_id"`
TriggerEvidenceKind pgtype.Text `json:"trigger_evidence_kind"`
TriggerEvidenceRefID pgtype.UUID `json:"trigger_evidence_ref_id"`
}
// Deferred tasks are inert until PromoteDueDeferredTasksForRuntime flips them
// to queued. Used for comment-routing escalation: a thread-owner primary task
// gets a delayed assignee fallback without waking both agents at t=0.
// Attribution is resolved and stamped at creation (not at promotion), from the
// same trigger comment as the primary task, so the fallback assignee's run
// carries a non-NULL source and evidence rather than bypassing attribution
// (MUL-4302 §2).
func (q *Queries) CreateDeferredAgentTask(ctx context.Context, arg CreateDeferredAgentTaskParams) (AgentTaskQueue, error) {
row := q.db.QueryRow(ctx, createDeferredAgentTask,
arg.AgentID,
arg.RuntimeID,
arg.IssueID,
arg.Priority,
arg.TriggerCommentID,
arg.TriggerSummary,
arg.IsLeaderTask,
arg.SquadID,
arg.EscalationForTaskID,
arg.FireAt,
arg.OriginatorUserID,
arg.AccountableUserID,
arg.OriginatorSource,
arg.DelegatedFromTaskID,
arg.TriggerEvidenceKind,
arg.TriggerEvidenceRefID,
)
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,
)
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,
originator_source, trigger_evidence_kind, trigger_evidence_ref_id
)
VALUES (
$1, $2, NULL, 'queued', $3, $4,
$5,
$6,
$7,
$8,
$9,
$10,
$11
)
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
`
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"`
OriginatorSource pgtype.Text `json:"originator_source"`
TriggerEvidenceKind pgtype.Text `json:"trigger_evidence_kind"`
TriggerEvidenceRefID pgtype.UUID `json:"trigger_evidence_ref_id"`
}
// 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.OriginatorSource,
arg.TriggerEvidenceKind,
arg.TriggerEvidenceRefID,
)
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,
)
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,
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
)
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,
CASE WHEN p.failure_reason IS NOT DISTINCT FROM 'codex_semantic_inactivity' THEN NULL ELSE p.work_dir END,
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,
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
FROM agent_task_queue p
WHERE p.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
`
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"`
}
// Clones a parent task into a fresh queued attempt. Carries forward the
// agent's resume context (session_id/work_dir) so the child can continue
// the conversation when the backend supports it. Resume-unsafe failures are
// retried as fresh sessions so the child does not inherit a stuck agent
// conversation. Keep the CASE WHEN predicates in sync with
// resumeUnsafeFailureReason and the resume lookup blacklists. attempt is
// incremented; max_attempts, trigger_comment_id, coalesced_comment_ids,
// is_leader_task, and squad_id are inherited so the retried task receives the
// parent's complete planned comment batch and keeps the same squad-role
// provenance. delivered_comment_ids intentionally stays at its '{}' default:
// the child must earn its own delivery receipt at claim time.
//
// originator_user_id is inherited so the Composio overlay decision sees the
// same top-of-chain human across the retry: the user behind the original
// run has not changed. The Composio overlay follows the agent's invocation
// permission and uses the agent owner's connection (MUL-3963); originator is
// carried for A2A/audit, not as an originator == agent.owner_id gate.
// A system retry is NOT a new attribution event (MUL-4302 §5): it inherits the
// parent's accountable human, source label, delegation lineage, rule version,
// and trigger evidence UNCHANGED, and records retry_of_task_id = p.id so retry
// and manual rerun stay separable in reporting. parent_task_id keeps its
// existing meaning for the retry/resume machinery; retry_of_task_id is the
// attribution-facing lineage column.
//
// chat_input_task_id is inherited straight from the parent so the whole retry
// chain keeps consuming the ORIGINAL root input batch (MUL-4351): the root
// direct task set it to its own id, every descendant copies that value, and a
// claim always reads the same user messages. A plain copy (not
// COALESCE(parent.chat_input_task_id, parent.id)) is deliberate: legacy/channel
// parents carry NULL and must stay NULL so their retries keep the trailing
// selector — promoting a pre-migration NULL row to the task-owned path on retry
// would risk replaying untagged history during a rolling deploy.
//
// Chat retries are queued at GREATEST(priority, 3) so a transiently-failed
// earlier turn is re-claimed ahead of any fresh chat task (priority 2) the user
// queued while the failing turn was still running — the retry continues the
// older turn first. Combined with creating the retry inside FailTask's
// transaction, this leaves no window for a newer input task to jump ahead.
//
// fire_at arms a backoff before the retry: when non-NULL the child is inserted
// as 'deferred' with that fire_at and stays inert until the existing
// PromoteDueDeferredTasksForRuntime sweeper (run promote-first on every claim
// poll) flips it to 'queued'. Used for provider_network's final attempt so it
// waits ~5s instead of firing back-to-back with the immediate retry (MUL-4910).
// NULL keeps the historical behaviour: an immediately-claimable 'queued' child.
//
// max_attempts overrides the inherited budget when non-NULL (NULL inherits
// p.max_attempts unchanged). Callers persist the reason-aware effective ceiling
// here so the row stays self-consistent — e.g. provider_network's chain records
// attempt=3, max_attempts=3 rather than leaking attempt=3, max_attempts=2 to the
// task API (MUL-4910). The Go retryAttemptCeiling already refuses to raise a
// disabled (max_attempts<=1) task, so this only ever widens, never revives.
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,
)
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,
)
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 expireStaleQueuedTasks = `-- name: ExpireStaleQueuedTasks :many
WITH victims AS (
SELECT id FROM agent_task_queue
WHERE status = 'queued'
AND created_at < now() - make_interval(secs => $1::double precision)
ORDER BY created_at ASC
LIMIT $2::int
FOR UPDATE SKIP LOCKED
)
UPDATE agent_task_queue t
SET status = 'failed',
completed_at = now(),
error = 'task expired in queue',
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)
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
`
type ExpireStaleQueuedTasksParams struct {
TtlSecs float64 `json:"ttl_secs"`
MaxPerTick int32 `json:"max_per_tick"`
}
// Fails tasks that have been sitting in 'queued' for longer than the TTL.
// This is the cleanup arm of the MUL-1899 "queued backlog" fix: even with the
// new dispatch-time admission gate that refuses to enqueue when the runtime
// is offline, we still need to drain the historical 87k+ doomed rows and
// handle edge cases where a runtime goes offline AFTER a task is already
// queued (the admission check protects new enqueues, not in-flight queue
// depth).
//
// Concurrency safety: the daemon's claim path may race with this sweeper to
// transition the same row out of 'queued'. We protect against that two
// ways:
// 1. The CTE selects victims with FOR UPDATE SKIP LOCKED so a row that is
// currently being claimed (or otherwise locked) is skipped — no lock
// contention with the dispatch path, and we won't queue up behind it.
// 2. The outer UPDATE re-checks status='queued' AND the TTL predicate at
// apply time. If a daemon claimed the row between selection and update
// (e.g. lock released after the claim transaction commits), the row is
// already 'dispatched'/'running' and the WHERE clause filters it out
// so we cannot clobber an in-flight task.
//
// Capped via LIMIT inside the CTE so a single sweep tick cannot monopolise
// the DB when the backlog is large — the sweeper drains the rest on
// subsequent ticks.
func (q *Queries) ExpireStaleQueuedTasks(ctx context.Context, arg ExpireStaleQueuedTasksParams) ([]AgentTaskQueue, error) {
rows, err := q.db.Query(ctx, expireStaleQueuedTasks, arg.TtlSecs, 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,
); 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
`
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,
)
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),
session_rollout_missing = $4,
retired_session_id = COALESCE($7, 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
`
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"`
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.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,
)
return i, err
}
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())
)
OR (
status = 'running'
AND started_at < now() - make_interval(secs => $2::double precision)
AND NOT EXISTS (
SELECT 1 FROM agent_runtime r
WHERE r.id = agent_task_queue.runtime_id
AND r.status = 'online'
AND r.last_seen_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
`
type FailStaleTasksParams struct {
DispatchTimeoutSecs float64 `json:"dispatch_timeout_secs"`
RunningTimeoutSecs float64 `json:"running_timeout_secs"`
RuntimeStaleSecs float64 `json:"runtime_stale_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.
//
// - 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 whose
// runtime is `online` AND whose `last_seen_at` is within
// @runtime_stale_secs is treated as alive and is NOT killed by this
// wall-clock backstop, even after `started_at` exceeds the running
// timeout. This is what lets healthy multi-hour research / training runs
// survive on self-hosted deployments (MUL-4107): the daemon side is
// bounded only by inactivity watchdogs (idle / per-tool), so the
// server-side wall clock must not shadow that with a coarser cap.
//
// The daemon-dead case is the primary responsibility of `sweepStaleRuntimes`
// (which mixes DB `last_seen_at` with the Redis LivenessStore and calls
// `FailTasksForOfflineRuntimes` in the same tick). The wall-clock branch
// here is a defensive backstop for pathological cases where a runtime row
// somehow retains status='online' with a stale DB heartbeat for longer than
// the wall clock allows.
//
// 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.RunningTimeoutSecs, 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,
); 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 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,
)
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 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,
)
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 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,
)
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 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,
)
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 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,
)
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 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,
)
return i, 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 r.retired_session_id IS NOT NULL
), 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,
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 t.session_id IS NOT NULL
AND t.status IN ('completed', 'failed')
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 FROM latest_per_session
WHERE session_id NOT IN (SELECT session_id FROM retired_sessions)
AND (
status = 'completed'
OR (
status = 'failed'
AND COALESCE(failure_reason, '') NOT IN ('iteration_limit', 'agent_fallback_message', 'api_invalid_request', 'codex_semantic_inactivity', 'agent_error.context_overflow')
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%')
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]')
)
)
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"`
}
// Returns the session_id and work_dir from the most recent task for a given
// (agent_id, issue_id) pair, used for session resumption on the auto-retry
// path. We accept both 'completed' and 'failed' tasks: a failed task may
// have established a real agent session before crashing (orphaned by a
// daemon restart, runtime offline, or sweeper timeout), and the daemon pins
// the resume pointer mid-flight via UpdateAgentTaskSession. Without this,
// an auto-retry of a mid-run failure would silently start a fresh
// conversation and lose the in-flight context — exactly what MUL-1128's B
// branch is meant to fix.
//
// Manual rerun (TaskService.RerunIssue) does NOT take this path. The claim
// handler branches on rerun_of_task_id FIRST and resolves the session/workdir
// from that exact source task (so a parallel task on the same issue can't be
// resumed by mistake), reusing the source workdir and resuming its session only
// when the source failure is resume-safe. The rerun row still carries
// force_fresh_session=true purely as a rollback-safe signal: an OLD claim
// handler that predates the rerun_of_task_id branch falls back to this query,
// and force_fresh_session=true makes it start clean instead of resuming the
// wrong execution (MUL-4869).
//
// Tasks that ended in a known "poisoned" terminal state are also excluded
// here so even auto-retry does not inherit the bad session. The daemon
// classifies these failures (iteration_limit, agent_fallback_message,
// api_invalid_request, codex_semantic_inactivity, agent_error.context_overflow)
// when it detects either an agent fallback marker in the output, an upstream
// API 400 that means the conversation history itself is unprocessable
// (oversized image, malformed base64, etc.), a Codex semantic inactivity
// timeout whose recorded session may replay the same stuck state, or a context
// window overflow that would immediately overflow again on resume. Keep this
// list in sync with resumeUnsafeFailureReason and GetLastChatTaskSession.
//
// The error-text ILIKE clause is defense-in-depth for the api_invalid_request
// shape: a legacy row tagged 'agent_error' (pre-MUL-1921), a deploy-window
// row that the old code wrote between migration and rollout, or a future
// error format that escapes the daemon classifier all still get filtered
// here as long as the canonical Anthropic 400 marker is present in the
// error text. Migration 079 backfills the failure_reason column itself,
// so observability stays accurate; this clause guarantees session resume
// never picks up a bad session even when failure_reason hasn't caught up.
//
// Selection is per-session, not per-row: we first reduce to the single most
// recent terminal row FOR EACH session_id (DISTINCT ON), then apply the
// resume-unsafe filter. This closes a poisoning wormhole (GH #5975): when the
// SAME session_id has both an older 'completed' row (the turn that first baked
// in the bad history) and a newer poisoned 'failed' row, a plain row-level
// filter would drop the poisoned row and happily fall back to the older
// completed row — which points at the exact same dead session. Judging by each
// session's LATEST terminal state instead means:
// - a newer poisoned row invalidates the whole session, older completed rows
// included;
// - a different, healthy session (distinct id) is still eligible as a
// fallback;
// - if that same id later terminates cleanly again, the newer completed row
// restores its resumability.
//
// The oversized-image ILIKE (dimension phrase AND image.source.base64.data) is
// the GH #5975 analogue of the api_invalid_request defense-in-depth above: a
// Kiro/ACP resume rejected for an oversized historical image is classified
// 'api_invalid_request' at write time, but this text clause also blocks a
// legacy/unclassified row (or a new-server + old-daemon deploy window) from
// resuming the poisoned session. Both markers are required so the clause stays
// exactly as narrow as classifyPoisonedError and the Kiro detector — an
// unrelated error that only mentions image dimensions is NOT excluded.
//
// The final pair of regexes is the provider-agnostic version of the same
// guard, and it matters most for self-hosted installs: daemons upgrade on
// their own cadence, so a host still running a daemon without
// taskfailure.UnresumableHistory reports an empty-message rejection as
// agent_error.unknown and would otherwise keep resuming the transcript the
// provider just refused (GH #6066). Both regexes must match — an emptiness
// complaint AND a locator pointing into the message history — mirroring
// emptyContentRe and historyMessageLocatorRe in pkg/taskfailure/resume.go.
// Keep the three in sync.
//
// retired_sessions is the explicit half of the same rule (GH #6066). The
// per-session latest state above can only judge sessions that some row still
// POINTS at, so it cannot see a session a run deliberately abandoned: a fresh
// retry that succeeded reports its own new id (or none), leaving the poisoned
// id visible on nothing but an older completed row, which then looks perfectly
// healthy. retired_session_id records the abandonment itself, so one row
// retiring a session removes it from every later lookup no matter how many
// clean rows still reference it.
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)
return i, err
}
const getLastTaskStartedAtForIssueAndAgent = `-- name: GetLastTaskStartedAtForIssueAndAgent :one
SELECT started_at FROM agent_task_queue
WHERE agent_id = $1 AND issue_id = $2 AND started_at IS NOT NULL
ORDER BY started_at DESC
LIMIT 1
`
type GetLastTaskStartedAtForIssueAndAgentParams struct {
AgentID pgtype.UUID `json:"agent_id"`
IssueID pgtype.UUID `json:"issue_id"`
}
// Returns the started_at of the most recent prior task for this (agent, issue)
// pair, used as the "since" anchor for counting comments that arrived since the
// agent's last run. Any terminal state counts as "a run happened". Tasks with
// no started_at (never dispatched / the just-claimed current task) are excluded,
// so this never returns the current claim's own row. MUST use started_at, never
// completed_at: a long run would otherwise miss comments posted while it ran.
func (q *Queries) GetLastTaskStartedAtForIssueAndAgent(ctx context.Context, arg GetLastTaskStartedAtForIssueAndAgentParams) (pgtype.Timestamptz, error) {
row := q.db.QueryRow(ctx, getLastTaskStartedAtForIssueAndAgent, arg.AgentID, arg.IssueID)
var started_at pgtype.Timestamptz
err := row.Scan(&started_at)
return started_at, err
}
const getLatestChatTaskRolloutMissing = `-- name: GetLatestChatTaskRolloutMissing :one
SELECT COALESCE(session_rollout_missing, FALSE) FROM agent_task_queue
WHERE chat_session_id = $1
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
`
// 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, chatSessionID pgtype.UUID) (bool, error) {
row := q.db.QueryRow(ctx, getLatestChatTaskRolloutMissing, chatSessionID)
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 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.
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
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"`
}
// 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.
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,
); 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')
`
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, or waiting_local_directory. 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 hasPendingTaskForIssue = `-- name: HasPendingTaskForIssue :one
SELECT count(*) > 0 AS has_pending FROM agent_task_queue
WHERE 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 issue_id = $1 AND agent_id = $2 AND status IN ('queued', 'dispatched')
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. 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 issue_id = $1
AND agent_id = $2
AND status IN ('queued', 'dispatched')
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 linkTaskToIssue = `-- name: LinkTaskToIssue :exec
UPDATE agent_task_queue
SET issue_id = $2
WHERE id = $1 AND issue_id IS NULL
`
type LinkTaskToIssueParams struct {
ID pgtype.UUID `json:"id"`
IssueID pgtype.UUID `json:"issue_id"`
}
// 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 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 archived 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,
); 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 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 exactly
// the set ArchiveAgentsByIDs will operate on — no race window.
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,
); 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 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,
); 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 FROM agent_task_queue
WHERE agent_id = $1
ORDER BY created_at DESC
`
func (q *Queries) ListAgentTasks(ctx context.Context, agentID pgtype.UUID) ([]AgentTaskQueue, error) {
rows, err := q.db.Query(ctx, listAgentTasks, 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,
); 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 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,
); 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 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,
); 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 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,
); 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 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,
); 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 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,
); 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 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 FROM agent_task_queue
WHERE runtime_id = $1 AND status = 'queued'
ORDER BY priority DESC, created_at ASC
`
// Returns rows the runtime can 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,
); 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 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 FROM agent_task_queue
WHERE runtime_id = ANY($1::uuid[]) AND status = 'queued'
ORDER BY priority DESC, 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,
); 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 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,
); 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 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')
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 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
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.
// Row shape and row set are unchanged.
//
// 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,
); 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 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
`
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,
)
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
`
// 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,
)
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
WHERE id = (
SELECT t.id FROM agent_task_queue t
WHERE t.issue_id = $12
AND t.agent_id = $13
AND t.status = 'queued'
-- Head-scoped (TEN-356, #5914): never fold across HEADs. The physical
-- unique index is only (issue_id, agent_id), 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($14::text, '') = ''
OR t.context->>'head_sha' = $14::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"`
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 the single 'queued' task on purpose (MUL-4195 review
// rounds 24). This merge is only reached when HasPendingTaskForIssueAndAgent
// matched a 'queued'/'dispatched' task, and '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. 'deferred' is also NOT
// a target: a deferred row is an assignee-fallback escalation with its own
// fire_at/promotion lifecycle, and it never sets AlreadyPending
// (HasPendingTaskForIssueAndAgent only looks at queued/dispatched). If a newer
// deferred fallback and an older queued task coexisted, a status-IN target would
// pick the deferred one by created_at and steal the coalescing target away from
// the queued run that is actually about to be claimed — so we match ONLY the
// queued row (the idx_one_pending_task_per_issue_agent unique index guarantees
// at most one). 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.IssueID,
arg.AgentID,
arg.HeadSha,
)
var i MergeCommentIntoPendingTaskRow
err := row.Scan(&i.ID, &i.CoalescedCommentIds)
return i, err
}
const promoteDueDeferredTasksForRuntime = `-- name: PromoteDueDeferredTasksForRuntime :many
UPDATE agent_task_queue
SET status = 'queued'
WHERE runtime_id = $1
AND status = 'deferred'
AND fire_at <= now()
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
`
func (q *Queries) PromoteDueDeferredTasksForRuntime(ctx context.Context, runtimeID pgtype.UUID) ([]AgentTaskQueue, error) {
rows, err := q.db.Query(ctx, promoteDueDeferredTasksForRuntime, 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,
); 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
UPDATE agent_task_queue
SET status = 'queued'
WHERE runtime_id = ANY($1::uuid[])
AND status = 'deferred'
AND fire_at <= now()
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
`
// Batch variant of PromoteDueDeferredTasksForRuntime (MUL-4257): promotes all
// due deferred tasks across the runtime set in one UPDATE.
func (q *Queries) PromoteDueDeferredTasksForRuntimes(ctx context.Context, runtimeIds []pgtype.UUID) ([]AgentTaskQueue, error) {
rows, err := q.db.Query(ctx, promoteDueDeferredTasksForRuntimes, 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,
); 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
`
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,
)
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())
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
`
type ReclaimStaleDispatchedTaskForRuntimeParams struct {
RuntimeID pgtype.UUID `json:"runtime_id"`
PrepareLeaseSecs float64 `json:"prepare_lease_secs"`
ClaimRecoverySecs float64 `json:"claim_recovery_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)
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,
)
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())
ORDER BY atq.priority DESC, atq.dispatched_at ASC
LIMIT $4::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
`
type ReclaimStaleDispatchedTasksForRuntimesParams struct {
PrepareLeaseSecs float64 `json:"prepare_lease_secs"`
RuntimeIds []pgtype.UUID `json:"runtime_ids"`
ClaimRecoverySecs float64 `json:"claim_recovery_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.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,
); 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
`
// 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,
); 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
UPDATE agent AS a
SET status = CASE WHEN EXISTS (
SELECT 1 FROM agent_task_queue q
WHERE q.agent_id = a.id AND q.status IN ('dispatched', 'running', 'waiting_local_directory')
) THEN 'working' ELSE 'idle' END,
updated_at = now()
WHERE a.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
`
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,
)
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.issue_id = $2
AND t.agent_id = $3
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
)
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.
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
`
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,
)
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
`
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,
)
return i, err
}
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 startAgentTask = `-- name: StartAgentTask :one
UPDATE agent_task_queue
SET status = 'running',
started_at = now(),
wait_reason = NULL,
prepare_lease_expires_at = NULL
WHERE id = $1 AND 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
`
// 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,
)
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),
composio_toolkit_allowlist = COALESCE($19::text[], composio_toolkit_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
`
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"`
ComposioToolkitAllowlist []string `json:"composio_toolkit_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.ComposioToolkitAllowlist,
)
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,
)
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
`
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,
)
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
`
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,
)
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
`
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,
)
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')
`
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.
func (q *Queries) UpdateAgentTaskSession(ctx context.Context, arg UpdateAgentTaskSessionParams) error {
_, err := q.db.Exec(ctx, updateAgentTaskSession, arg.ID, arg.SessionID, arg.WorkDir)
return err
}