mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-03 11:10:23 +02:00
* 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>
1960 lines
62 KiB
Go
1960 lines
62 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: autopilot.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const addAutopilotCollaborator = `-- name: AddAutopilotCollaborator :one
|
|
INSERT INTO autopilot_collaborator (autopilot_id, user_type, user_id, granted_by)
|
|
VALUES ($1, $2, $3, $4)
|
|
ON CONFLICT (autopilot_id, user_type, user_id)
|
|
DO UPDATE SET granted_by = EXCLUDED.granted_by
|
|
RETURNING autopilot_id, user_type, user_id, granted_by, created_at
|
|
`
|
|
|
|
type AddAutopilotCollaboratorParams struct {
|
|
AutopilotID pgtype.UUID `json:"autopilot_id"`
|
|
UserType string `json:"user_type"`
|
|
UserID pgtype.UUID `json:"user_id"`
|
|
GrantedBy pgtype.UUID `json:"granted_by"`
|
|
}
|
|
|
|
// Re-granting an existing collaborator is a no-op that refreshes granted_by,
|
|
// so the call is idempotent from the API boundary.
|
|
func (q *Queries) AddAutopilotCollaborator(ctx context.Context, arg AddAutopilotCollaboratorParams) (AutopilotCollaborator, error) {
|
|
row := q.db.QueryRow(ctx, addAutopilotCollaborator,
|
|
arg.AutopilotID,
|
|
arg.UserType,
|
|
arg.UserID,
|
|
arg.GrantedBy,
|
|
)
|
|
var i AutopilotCollaborator
|
|
err := row.Scan(
|
|
&i.AutopilotID,
|
|
&i.UserType,
|
|
&i.UserID,
|
|
&i.GrantedBy,
|
|
&i.CreatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const addAutopilotSubscriber = `-- name: AddAutopilotSubscriber :exec
|
|
INSERT INTO autopilot_subscriber (autopilot_id, user_type, user_id)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (autopilot_id, user_type, user_id) DO NOTHING
|
|
`
|
|
|
|
type AddAutopilotSubscriberParams struct {
|
|
AutopilotID pgtype.UUID `json:"autopilot_id"`
|
|
UserType string `json:"user_type"`
|
|
UserID pgtype.UUID `json:"user_id"`
|
|
}
|
|
|
|
func (q *Queries) AddAutopilotSubscriber(ctx context.Context, arg AddAutopilotSubscriberParams) error {
|
|
_, err := q.db.Exec(ctx, addAutopilotSubscriber, arg.AutopilotID, arg.UserType, arg.UserID)
|
|
return err
|
|
}
|
|
|
|
const advanceTriggerNextRun = `-- name: AdvanceTriggerNextRun :exec
|
|
UPDATE autopilot_trigger
|
|
SET next_run_at = $2,
|
|
last_fired_at = now(),
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
`
|
|
|
|
type AdvanceTriggerNextRunParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
NextRunAt pgtype.Timestamptz `json:"next_run_at"`
|
|
}
|
|
|
|
func (q *Queries) AdvanceTriggerNextRun(ctx context.Context, arg AdvanceTriggerNextRunParams) error {
|
|
_, err := q.db.Exec(ctx, advanceTriggerNextRun, arg.ID, arg.NextRunAt)
|
|
return err
|
|
}
|
|
|
|
const archiveAutopilot = `-- name: ArchiveAutopilot :exec
|
|
UPDATE autopilot
|
|
SET status = 'archived', updated_at = now()
|
|
WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) ArchiveAutopilot(ctx context.Context, id pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, archiveAutopilot, id)
|
|
return err
|
|
}
|
|
|
|
const createAutopilot = `-- name: CreateAutopilot :one
|
|
INSERT INTO autopilot (
|
|
workspace_id, title, description, assignee_type, assignee_id,
|
|
status, execution_mode, issue_title_template, project_id,
|
|
created_by_type, created_by_id
|
|
) VALUES (
|
|
$1, $2, $9, $3, $4,
|
|
$5, $6, $10, $11,
|
|
$7, $8
|
|
) RETURNING id, workspace_id, title, description, assignee_id, status, execution_mode, issue_title_template, created_by_type, created_by_id, last_run_at, created_at, updated_at, assignee_type, project_id
|
|
`
|
|
|
|
type CreateAutopilotParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Title string `json:"title"`
|
|
AssigneeType string `json:"assignee_type"`
|
|
AssigneeID pgtype.UUID `json:"assignee_id"`
|
|
Status string `json:"status"`
|
|
ExecutionMode string `json:"execution_mode"`
|
|
CreatedByType string `json:"created_by_type"`
|
|
CreatedByID pgtype.UUID `json:"created_by_id"`
|
|
Description pgtype.Text `json:"description"`
|
|
IssueTitleTemplate pgtype.Text `json:"issue_title_template"`
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
}
|
|
|
|
func (q *Queries) CreateAutopilot(ctx context.Context, arg CreateAutopilotParams) (Autopilot, error) {
|
|
row := q.db.QueryRow(ctx, createAutopilot,
|
|
arg.WorkspaceID,
|
|
arg.Title,
|
|
arg.AssigneeType,
|
|
arg.AssigneeID,
|
|
arg.Status,
|
|
arg.ExecutionMode,
|
|
arg.CreatedByType,
|
|
arg.CreatedByID,
|
|
arg.Description,
|
|
arg.IssueTitleTemplate,
|
|
arg.ProjectID,
|
|
)
|
|
var i Autopilot
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.AssigneeID,
|
|
&i.Status,
|
|
&i.ExecutionMode,
|
|
&i.IssueTitleTemplate,
|
|
&i.CreatedByType,
|
|
&i.CreatedByID,
|
|
&i.LastRunAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.AssigneeType,
|
|
&i.ProjectID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createAutopilotRuleVersion = `-- name: CreateAutopilotRuleVersion :one
|
|
|
|
INSERT INTO autopilot_rule_version (
|
|
autopilot_id, workspace_id, published_by_type, published_by_id, config_summary
|
|
)
|
|
VALUES (
|
|
$1, $2, $3, $4,
|
|
COALESCE($5, '{}'::jsonb)
|
|
)
|
|
RETURNING id, autopilot_id, workspace_id, published_by_type, published_by_id, config_summary, created_at
|
|
`
|
|
|
|
type CreateAutopilotRuleVersionParams struct {
|
|
AutopilotID pgtype.UUID `json:"autopilot_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
PublishedByType string `json:"published_by_type"`
|
|
PublishedByID pgtype.UUID `json:"published_by_id"`
|
|
ConfigSummary interface{} `json:"config_summary"`
|
|
}
|
|
|
|
// =====================
|
|
// Autopilot Rule Version (rule_owner attribution, MUL-4302 §3.4)
|
|
// =====================
|
|
// Append one immutable rule-version snapshot on a substantive publish (create /
|
|
// enable / resume / target / execution-mode change). published_by_* is the acting
|
|
// member (or 'system' with NULL id for the failure monitor); config_summary is the
|
|
// effective config at publish time. Dispatch reads the latest row for the autopilot
|
|
// as the run's rule_owner accountable human.
|
|
func (q *Queries) CreateAutopilotRuleVersion(ctx context.Context, arg CreateAutopilotRuleVersionParams) (AutopilotRuleVersion, error) {
|
|
row := q.db.QueryRow(ctx, createAutopilotRuleVersion,
|
|
arg.AutopilotID,
|
|
arg.WorkspaceID,
|
|
arg.PublishedByType,
|
|
arg.PublishedByID,
|
|
arg.ConfigSummary,
|
|
)
|
|
var i AutopilotRuleVersion
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.WorkspaceID,
|
|
&i.PublishedByType,
|
|
&i.PublishedByID,
|
|
&i.ConfigSummary,
|
|
&i.CreatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createAutopilotRun = `-- name: CreateAutopilotRun :one
|
|
|
|
INSERT INTO autopilot_run (
|
|
autopilot_id, trigger_id, source, status, trigger_payload, squad_id, planned_at,
|
|
webhook_delivery_id
|
|
) VALUES (
|
|
$1, $4, $2, $3, $5,
|
|
$6, $7,
|
|
$8
|
|
) RETURNING id, autopilot_id, trigger_id, source, status, issue_id, task_id, triggered_at, completed_at, failure_reason, trigger_payload, result, created_at, squad_id, planned_at, webhook_delivery_id
|
|
`
|
|
|
|
type CreateAutopilotRunParams struct {
|
|
AutopilotID pgtype.UUID `json:"autopilot_id"`
|
|
Source string `json:"source"`
|
|
Status string `json:"status"`
|
|
TriggerID pgtype.UUID `json:"trigger_id"`
|
|
TriggerPayload []byte `json:"trigger_payload"`
|
|
SquadID pgtype.UUID `json:"squad_id"`
|
|
PlannedAt pgtype.Timestamptz `json:"planned_at"`
|
|
WebhookDeliveryID pgtype.UUID `json:"webhook_delivery_id"`
|
|
}
|
|
|
|
// =====================
|
|
// Autopilot Run Management
|
|
// =====================
|
|
// squad_id is an attribution hook: set to the assignee squad when the
|
|
// parent autopilot has assignee_type='squad', NULL otherwise. The executing
|
|
// agent_id on agent_task_queue still records who actually ran the work
|
|
// (the squad leader); squad_id lets reports group by squad without a join.
|
|
//
|
|
// planned_at carries the canonical UTC fire time for scheduled triggers
|
|
// (source='schedule'); it stays NULL for manual / webhook / api sources
|
|
// which have no canonical occurrence. Combined with the partial unique
|
|
// index uq_autopilot_run_trigger_planned, this gives dispatch-layer
|
|
// idempotency: a stale-steal retry at the same plan_time cannot create
|
|
// a second run for the same (trigger_id, planned_at) pair (MUL-3551).
|
|
func (q *Queries) CreateAutopilotRun(ctx context.Context, arg CreateAutopilotRunParams) (AutopilotRun, error) {
|
|
row := q.db.QueryRow(ctx, createAutopilotRun,
|
|
arg.AutopilotID,
|
|
arg.Source,
|
|
arg.Status,
|
|
arg.TriggerID,
|
|
arg.TriggerPayload,
|
|
arg.SquadID,
|
|
arg.PlannedAt,
|
|
arg.WebhookDeliveryID,
|
|
)
|
|
var i AutopilotRun
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.TriggerID,
|
|
&i.Source,
|
|
&i.Status,
|
|
&i.IssueID,
|
|
&i.TaskID,
|
|
&i.TriggeredAt,
|
|
&i.CompletedAt,
|
|
&i.FailureReason,
|
|
&i.TriggerPayload,
|
|
&i.Result,
|
|
&i.CreatedAt,
|
|
&i.SquadID,
|
|
&i.PlannedAt,
|
|
&i.WebhookDeliveryID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createAutopilotTask = `-- name: CreateAutopilotTask :one
|
|
|
|
INSERT INTO agent_task_queue (
|
|
agent_id, runtime_id, issue_id, status, priority, autopilot_run_id, trigger_summary,
|
|
originator_user_id, accountable_user_id, rule_version_id,
|
|
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 CreateAutopilotTaskParams struct {
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
RuntimeID pgtype.UUID `json:"runtime_id"`
|
|
Priority int32 `json:"priority"`
|
|
AutopilotRunID pgtype.UUID `json:"autopilot_run_id"`
|
|
TriggerSummary pgtype.Text `json:"trigger_summary"`
|
|
OriginatorUserID pgtype.UUID `json:"originator_user_id"`
|
|
AccountableUserID pgtype.UUID `json:"accountable_user_id"`
|
|
RuleVersionID pgtype.UUID `json:"rule_version_id"`
|
|
OriginatorSource pgtype.Text `json:"originator_source"`
|
|
TriggerEvidenceKind pgtype.Text `json:"trigger_evidence_kind"`
|
|
TriggerEvidenceRefID pgtype.UUID `json:"trigger_evidence_ref_id"`
|
|
}
|
|
|
|
// =====================
|
|
// Task Queue (run_only mode)
|
|
// =====================
|
|
// run_only autopilot dispatch. Attribution depends on the trigger:
|
|
// - schedule / webhook / api: no human authorized the run, so originator_user_id
|
|
// stays NULL and accountable_user_id is the rule_owner (the publisher of the
|
|
// autopilot's active rule version), with rule_version_id recording the snapshot
|
|
// (MUL-4302 §3.4) — the accountable-diverges-from-originator case.
|
|
// - manual: a member clicked "run now", a direct human action, so originator and
|
|
// accountable are BOTH that member (originator_source='direct_human'); no rule
|
|
// version is involved (MUL-4302 §4).
|
|
//
|
|
// When no version/publisher resolves on the non-manual path, the caller passes NULL
|
|
// accountable + originator_source='unattributed' so the row is still not a
|
|
// NULL-source bypass (MUL-4302 §2).
|
|
func (q *Queries) CreateAutopilotTask(ctx context.Context, arg CreateAutopilotTaskParams) (AgentTaskQueue, error) {
|
|
row := q.db.QueryRow(ctx, createAutopilotTask,
|
|
arg.AgentID,
|
|
arg.RuntimeID,
|
|
arg.Priority,
|
|
arg.AutopilotRunID,
|
|
arg.TriggerSummary,
|
|
arg.OriginatorUserID,
|
|
arg.AccountableUserID,
|
|
arg.RuleVersionID,
|
|
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 createAutopilotTrigger = `-- name: CreateAutopilotTrigger :one
|
|
INSERT INTO autopilot_trigger (
|
|
autopilot_id, kind, enabled, cron_expression, timezone,
|
|
next_run_at, webhook_token, label, provider, event_filters,
|
|
published_by_type, published_by_id
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5,
|
|
$6, $7, $8,
|
|
COALESCE($9::text, 'generic'),
|
|
$10,
|
|
$11, $12
|
|
) RETURNING id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters, published_by_type, published_by_id
|
|
`
|
|
|
|
type CreateAutopilotTriggerParams struct {
|
|
AutopilotID pgtype.UUID `json:"autopilot_id"`
|
|
Kind string `json:"kind"`
|
|
Enabled bool `json:"enabled"`
|
|
CronExpression pgtype.Text `json:"cron_expression"`
|
|
Timezone pgtype.Text `json:"timezone"`
|
|
NextRunAt pgtype.Timestamptz `json:"next_run_at"`
|
|
WebhookToken pgtype.Text `json:"webhook_token"`
|
|
Label pgtype.Text `json:"label"`
|
|
Provider pgtype.Text `json:"provider"`
|
|
EventFilters []byte `json:"event_filters"`
|
|
PublishedByType pgtype.Text `json:"published_by_type"`
|
|
PublishedByID pgtype.UUID `json:"published_by_id"`
|
|
}
|
|
|
|
func (q *Queries) CreateAutopilotTrigger(ctx context.Context, arg CreateAutopilotTriggerParams) (AutopilotTrigger, error) {
|
|
row := q.db.QueryRow(ctx, createAutopilotTrigger,
|
|
arg.AutopilotID,
|
|
arg.Kind,
|
|
arg.Enabled,
|
|
arg.CronExpression,
|
|
arg.Timezone,
|
|
arg.NextRunAt,
|
|
arg.WebhookToken,
|
|
arg.Label,
|
|
arg.Provider,
|
|
arg.EventFilters,
|
|
arg.PublishedByType,
|
|
arg.PublishedByID,
|
|
)
|
|
var i AutopilotTrigger
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.Kind,
|
|
&i.Enabled,
|
|
&i.CronExpression,
|
|
&i.Timezone,
|
|
&i.NextRunAt,
|
|
&i.WebhookToken,
|
|
&i.Label,
|
|
&i.LastFiredAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Provider,
|
|
&i.SigningSecret,
|
|
&i.EventFilters,
|
|
&i.PublishedByType,
|
|
&i.PublishedByID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteAutopilotCollaborator = `-- name: DeleteAutopilotCollaborator :exec
|
|
DELETE FROM autopilot_collaborator
|
|
WHERE autopilot_id = $1 AND user_type = $2 AND user_id = $3
|
|
`
|
|
|
|
type DeleteAutopilotCollaboratorParams struct {
|
|
AutopilotID pgtype.UUID `json:"autopilot_id"`
|
|
UserType string `json:"user_type"`
|
|
UserID pgtype.UUID `json:"user_id"`
|
|
}
|
|
|
|
func (q *Queries) DeleteAutopilotCollaborator(ctx context.Context, arg DeleteAutopilotCollaboratorParams) error {
|
|
_, err := q.db.Exec(ctx, deleteAutopilotCollaborator, arg.AutopilotID, arg.UserType, arg.UserID)
|
|
return err
|
|
}
|
|
|
|
const deleteAutopilotCollaboratorsForAutopilot = `-- name: DeleteAutopilotCollaboratorsForAutopilot :exec
|
|
DELETE FROM autopilot_collaborator
|
|
WHERE autopilot_id = $1
|
|
`
|
|
|
|
// Application-layer cleanup run inside the autopilot delete transaction.
|
|
func (q *Queries) DeleteAutopilotCollaboratorsForAutopilot(ctx context.Context, autopilotID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteAutopilotCollaboratorsForAutopilot, autopilotID)
|
|
return err
|
|
}
|
|
|
|
const deleteAutopilotSubscribersForAutopilot = `-- name: DeleteAutopilotSubscribersForAutopilot :exec
|
|
DELETE FROM autopilot_subscriber
|
|
WHERE autopilot_id = $1
|
|
`
|
|
|
|
// Paired with a re-insert loop to implement full-replace PATCH semantics.
|
|
func (q *Queries) DeleteAutopilotSubscribersForAutopilot(ctx context.Context, autopilotID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteAutopilotSubscribersForAutopilot, autopilotID)
|
|
return err
|
|
}
|
|
|
|
const deleteAutopilotTrigger = `-- name: DeleteAutopilotTrigger :exec
|
|
DELETE FROM autopilot_trigger WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteAutopilotTrigger(ctx context.Context, id pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteAutopilotTrigger, id)
|
|
return err
|
|
}
|
|
|
|
const failAutopilotRunsByIssue = `-- name: FailAutopilotRunsByIssue :exec
|
|
UPDATE autopilot_run
|
|
SET status = 'failed', completed_at = now(), failure_reason = 'linked issue was deleted'
|
|
WHERE issue_id = $1
|
|
AND status IN ('issue_created', 'running')
|
|
`
|
|
|
|
// Fails active autopilot runs linked to a given issue.
|
|
// Must be called BEFORE issue deletion (ON DELETE SET NULL clears issue_id).
|
|
func (q *Queries) FailAutopilotRunsByIssue(ctx context.Context, issueID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, failAutopilotRunsByIssue, issueID)
|
|
return err
|
|
}
|
|
|
|
const getActiveAutopilotRuleVersion = `-- name: GetActiveAutopilotRuleVersion :one
|
|
SELECT id, autopilot_id, workspace_id, published_by_type, published_by_id, config_summary, created_at FROM autopilot_rule_version
|
|
WHERE workspace_id = $1 AND autopilot_id = $2
|
|
ORDER BY created_at DESC
|
|
LIMIT 1
|
|
`
|
|
|
|
type GetActiveAutopilotRuleVersionParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
AutopilotID pgtype.UUID `json:"autopilot_id"`
|
|
}
|
|
|
|
// The active version is the newest published row for the autopilot. Scoped by
|
|
// workspace_id per the workspace query rule; autopilot_id is globally unique so the
|
|
// workspace filter is a guard, not the selector.
|
|
func (q *Queries) GetActiveAutopilotRuleVersion(ctx context.Context, arg GetActiveAutopilotRuleVersionParams) (AutopilotRuleVersion, error) {
|
|
row := q.db.QueryRow(ctx, getActiveAutopilotRuleVersion, arg.WorkspaceID, arg.AutopilotID)
|
|
var i AutopilotRuleVersion
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.WorkspaceID,
|
|
&i.PublishedByType,
|
|
&i.PublishedByID,
|
|
&i.ConfigSummary,
|
|
&i.CreatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getAutopilot = `-- name: GetAutopilot :one
|
|
SELECT id, workspace_id, title, description, assignee_id, status, execution_mode, issue_title_template, created_by_type, created_by_id, last_run_at, created_at, updated_at, assignee_type, project_id FROM autopilot
|
|
WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetAutopilot(ctx context.Context, id pgtype.UUID) (Autopilot, error) {
|
|
row := q.db.QueryRow(ctx, getAutopilot, id)
|
|
var i Autopilot
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.AssigneeID,
|
|
&i.Status,
|
|
&i.ExecutionMode,
|
|
&i.IssueTitleTemplate,
|
|
&i.CreatedByType,
|
|
&i.CreatedByID,
|
|
&i.LastRunAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.AssigneeType,
|
|
&i.ProjectID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getAutopilotInWorkspace = `-- name: GetAutopilotInWorkspace :one
|
|
SELECT id, workspace_id, title, description, assignee_id, status, execution_mode, issue_title_template, created_by_type, created_by_id, last_run_at, created_at, updated_at, assignee_type, project_id FROM autopilot
|
|
WHERE id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type GetAutopilotInWorkspaceParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) GetAutopilotInWorkspace(ctx context.Context, arg GetAutopilotInWorkspaceParams) (Autopilot, error) {
|
|
row := q.db.QueryRow(ctx, getAutopilotInWorkspace, arg.ID, arg.WorkspaceID)
|
|
var i Autopilot
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.AssigneeID,
|
|
&i.Status,
|
|
&i.ExecutionMode,
|
|
&i.IssueTitleTemplate,
|
|
&i.CreatedByType,
|
|
&i.CreatedByID,
|
|
&i.LastRunAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.AssigneeType,
|
|
&i.ProjectID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getAutopilotRun = `-- name: GetAutopilotRun :one
|
|
SELECT id, autopilot_id, trigger_id, source, status, issue_id, task_id, triggered_at, completed_at, failure_reason, trigger_payload, result, created_at, squad_id, planned_at, webhook_delivery_id FROM autopilot_run
|
|
WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetAutopilotRun(ctx context.Context, id pgtype.UUID) (AutopilotRun, error) {
|
|
row := q.db.QueryRow(ctx, getAutopilotRun, id)
|
|
var i AutopilotRun
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.TriggerID,
|
|
&i.Source,
|
|
&i.Status,
|
|
&i.IssueID,
|
|
&i.TaskID,
|
|
&i.TriggeredAt,
|
|
&i.CompletedAt,
|
|
&i.FailureReason,
|
|
&i.TriggerPayload,
|
|
&i.Result,
|
|
&i.CreatedAt,
|
|
&i.SquadID,
|
|
&i.PlannedAt,
|
|
&i.WebhookDeliveryID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getAutopilotRunByIssue = `-- name: GetAutopilotRunByIssue :one
|
|
|
|
SELECT id, autopilot_id, trigger_id, source, status, issue_id, task_id, triggered_at, completed_at, failure_reason, trigger_payload, result, created_at, squad_id, planned_at, webhook_delivery_id FROM autopilot_run
|
|
WHERE issue_id = $1 AND status IN ('issue_created', 'running')
|
|
LIMIT 1
|
|
`
|
|
|
|
// =====================
|
|
// Run lookup by linked entities
|
|
// =====================
|
|
func (q *Queries) GetAutopilotRunByIssue(ctx context.Context, issueID pgtype.UUID) (AutopilotRun, error) {
|
|
row := q.db.QueryRow(ctx, getAutopilotRunByIssue, issueID)
|
|
var i AutopilotRun
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.TriggerID,
|
|
&i.Source,
|
|
&i.Status,
|
|
&i.IssueID,
|
|
&i.TaskID,
|
|
&i.TriggeredAt,
|
|
&i.CompletedAt,
|
|
&i.FailureReason,
|
|
&i.TriggerPayload,
|
|
&i.Result,
|
|
&i.CreatedAt,
|
|
&i.SquadID,
|
|
&i.PlannedAt,
|
|
&i.WebhookDeliveryID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getAutopilotRunByTriggerAndPlanned = `-- name: GetAutopilotRunByTriggerAndPlanned :one
|
|
SELECT id, autopilot_id, trigger_id, source, status, issue_id, task_id, triggered_at, completed_at, failure_reason, trigger_payload, result, created_at, squad_id, planned_at, webhook_delivery_id FROM autopilot_run
|
|
WHERE trigger_id = $1
|
|
AND planned_at = $2
|
|
LIMIT 1
|
|
`
|
|
|
|
type GetAutopilotRunByTriggerAndPlannedParams struct {
|
|
TriggerID pgtype.UUID `json:"trigger_id"`
|
|
PlannedAt pgtype.Timestamptz `json:"planned_at"`
|
|
}
|
|
|
|
// Idempotent lookup used by DispatchAutopilotForPlan to detect a
|
|
// crash-during-dispatch retry: if a row already exists for this
|
|
// (trigger_id, planned_at), the caller reuses it instead of creating a
|
|
// duplicate. The partial unique index covers the same key, so a race
|
|
// between "look up then insert" still resolves to a single row — this
|
|
// query is just the fast path that lets us skip the INSERT when we
|
|
// can see the prior row clearly. Returns no rows for the (much more
|
|
// common) first-time dispatch.
|
|
func (q *Queries) GetAutopilotRunByTriggerAndPlanned(ctx context.Context, arg GetAutopilotRunByTriggerAndPlannedParams) (AutopilotRun, error) {
|
|
row := q.db.QueryRow(ctx, getAutopilotRunByTriggerAndPlanned, arg.TriggerID, arg.PlannedAt)
|
|
var i AutopilotRun
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.TriggerID,
|
|
&i.Source,
|
|
&i.Status,
|
|
&i.IssueID,
|
|
&i.TaskID,
|
|
&i.TriggeredAt,
|
|
&i.CompletedAt,
|
|
&i.FailureReason,
|
|
&i.TriggerPayload,
|
|
&i.Result,
|
|
&i.CreatedAt,
|
|
&i.SquadID,
|
|
&i.PlannedAt,
|
|
&i.WebhookDeliveryID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getAutopilotRunByWebhookDelivery = `-- name: GetAutopilotRunByWebhookDelivery :one
|
|
SELECT id, autopilot_id, trigger_id, source, status, issue_id, task_id, triggered_at, completed_at, failure_reason, trigger_payload, result, created_at, squad_id, planned_at, webhook_delivery_id FROM autopilot_run
|
|
WHERE webhook_delivery_id = $1
|
|
LIMIT 1
|
|
`
|
|
|
|
func (q *Queries) GetAutopilotRunByWebhookDelivery(ctx context.Context, webhookDeliveryID pgtype.UUID) (AutopilotRun, error) {
|
|
row := q.db.QueryRow(ctx, getAutopilotRunByWebhookDelivery, webhookDeliveryID)
|
|
var i AutopilotRun
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.TriggerID,
|
|
&i.Source,
|
|
&i.Status,
|
|
&i.IssueID,
|
|
&i.TaskID,
|
|
&i.TriggeredAt,
|
|
&i.CompletedAt,
|
|
&i.FailureReason,
|
|
&i.TriggerPayload,
|
|
&i.Result,
|
|
&i.CreatedAt,
|
|
&i.SquadID,
|
|
&i.PlannedAt,
|
|
&i.WebhookDeliveryID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getAutopilotTaskByRun = `-- name: GetAutopilotTaskByRun :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 autopilot_run_id = $1
|
|
ORDER BY created_at
|
|
LIMIT 1
|
|
`
|
|
|
|
// Repairs the narrow run_only crash window where the task INSERT committed
|
|
// but the following autopilot_run.task_id update did not.
|
|
func (q *Queries) GetAutopilotTaskByRun(ctx context.Context, autopilotRunID pgtype.UUID) (AgentTaskQueue, error) {
|
|
row := q.db.QueryRow(ctx, getAutopilotTaskByRun, autopilotRunID)
|
|
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 getAutopilotTrigger = `-- name: GetAutopilotTrigger :one
|
|
SELECT id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters, published_by_type, published_by_id FROM autopilot_trigger
|
|
WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetAutopilotTrigger(ctx context.Context, id pgtype.UUID) (AutopilotTrigger, error) {
|
|
row := q.db.QueryRow(ctx, getAutopilotTrigger, id)
|
|
var i AutopilotTrigger
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.Kind,
|
|
&i.Enabled,
|
|
&i.CronExpression,
|
|
&i.Timezone,
|
|
&i.NextRunAt,
|
|
&i.WebhookToken,
|
|
&i.Label,
|
|
&i.LastFiredAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Provider,
|
|
&i.SigningSecret,
|
|
&i.EventFilters,
|
|
&i.PublishedByType,
|
|
&i.PublishedByID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getWebhookTriggerByToken = `-- name: GetWebhookTriggerByToken :one
|
|
SELECT t.id, t.autopilot_id, t.kind, t.enabled, t.cron_expression, t.timezone, t.next_run_at, t.webhook_token, t.label, t.last_fired_at, t.created_at, t.updated_at, t.provider, t.signing_secret, t.event_filters, t.published_by_type, t.published_by_id, a.workspace_id AS autopilot_workspace_id
|
|
FROM autopilot_trigger t
|
|
JOIN autopilot a ON a.id = t.autopilot_id
|
|
WHERE t.kind = 'webhook'
|
|
AND t.webhook_token = $1
|
|
`
|
|
|
|
type GetWebhookTriggerByTokenRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
AutopilotID pgtype.UUID `json:"autopilot_id"`
|
|
Kind string `json:"kind"`
|
|
Enabled bool `json:"enabled"`
|
|
CronExpression pgtype.Text `json:"cron_expression"`
|
|
Timezone pgtype.Text `json:"timezone"`
|
|
NextRunAt pgtype.Timestamptz `json:"next_run_at"`
|
|
WebhookToken pgtype.Text `json:"webhook_token"`
|
|
Label pgtype.Text `json:"label"`
|
|
LastFiredAt pgtype.Timestamptz `json:"last_fired_at"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
Provider string `json:"provider"`
|
|
SigningSecret pgtype.Text `json:"signing_secret"`
|
|
EventFilters []byte `json:"event_filters"`
|
|
PublishedByType pgtype.Text `json:"published_by_type"`
|
|
PublishedByID pgtype.UUID `json:"published_by_id"`
|
|
AutopilotWorkspaceID pgtype.UUID `json:"autopilot_workspace_id"`
|
|
}
|
|
|
|
// Look up a webhook trigger by its public bearer token. Joined to autopilot
|
|
// so the webhook handler can derive the workspace from the trigger's parent
|
|
// without trusting any request header. The handler still re-loads the
|
|
// Autopilot via GetAutopilot and cross-checks WorkspaceID matches the row's
|
|
// autopilot_workspace_id.
|
|
func (q *Queries) GetWebhookTriggerByToken(ctx context.Context, webhookToken pgtype.Text) (GetWebhookTriggerByTokenRow, error) {
|
|
row := q.db.QueryRow(ctx, getWebhookTriggerByToken, webhookToken)
|
|
var i GetWebhookTriggerByTokenRow
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.Kind,
|
|
&i.Enabled,
|
|
&i.CronExpression,
|
|
&i.Timezone,
|
|
&i.NextRunAt,
|
|
&i.WebhookToken,
|
|
&i.Label,
|
|
&i.LastFiredAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Provider,
|
|
&i.SigningSecret,
|
|
&i.EventFilters,
|
|
&i.PublishedByType,
|
|
&i.PublishedByID,
|
|
&i.AutopilotWorkspaceID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const isAutopilotCollaborator = `-- name: IsAutopilotCollaborator :one
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM autopilot_collaborator
|
|
WHERE autopilot_id = $1 AND user_type = 'member' AND user_id = $2
|
|
) AS is_collaborator
|
|
`
|
|
|
|
type IsAutopilotCollaboratorParams struct {
|
|
AutopilotID pgtype.UUID `json:"autopilot_id"`
|
|
UserID pgtype.UUID `json:"user_id"`
|
|
}
|
|
|
|
func (q *Queries) IsAutopilotCollaborator(ctx context.Context, arg IsAutopilotCollaboratorParams) (bool, error) {
|
|
row := q.db.QueryRow(ctx, isAutopilotCollaborator, arg.AutopilotID, arg.UserID)
|
|
var is_collaborator bool
|
|
err := row.Scan(&is_collaborator)
|
|
return is_collaborator, err
|
|
}
|
|
|
|
const listAutopilotCollaborators = `-- name: ListAutopilotCollaborators :many
|
|
|
|
SELECT autopilot_id, user_type, user_id, granted_by, created_at FROM autopilot_collaborator
|
|
WHERE autopilot_id = $1
|
|
ORDER BY created_at ASC, user_id ASC
|
|
`
|
|
|
|
// =====================
|
|
// Autopilot Collaborators
|
|
// =====================
|
|
// ORDER BY created_at keeps row rendering stable across refreshes.
|
|
func (q *Queries) ListAutopilotCollaborators(ctx context.Context, autopilotID pgtype.UUID) ([]AutopilotCollaborator, error) {
|
|
rows, err := q.db.Query(ctx, listAutopilotCollaborators, autopilotID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []AutopilotCollaborator{}
|
|
for rows.Next() {
|
|
var i AutopilotCollaborator
|
|
if err := rows.Scan(
|
|
&i.AutopilotID,
|
|
&i.UserType,
|
|
&i.UserID,
|
|
&i.GrantedBy,
|
|
&i.CreatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAutopilotIDsForCollaborator = `-- name: ListAutopilotIDsForCollaborator :many
|
|
SELECT autopilot_id FROM autopilot_collaborator
|
|
WHERE user_type = 'member' AND user_id = $1
|
|
`
|
|
|
|
// Powers the per-row can_write flag on the list endpoint without an N+1.
|
|
func (q *Queries) ListAutopilotIDsForCollaborator(ctx context.Context, userID pgtype.UUID) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, listAutopilotIDsForCollaborator, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []pgtype.UUID{}
|
|
for rows.Next() {
|
|
var autopilot_id pgtype.UUID
|
|
if err := rows.Scan(&autopilot_id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, autopilot_id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAutopilotRuns = `-- name: ListAutopilotRuns :many
|
|
SELECT id, autopilot_id, trigger_id, source, status, issue_id, task_id, triggered_at, completed_at, failure_reason, trigger_payload, result, created_at, squad_id, planned_at, webhook_delivery_id FROM autopilot_run
|
|
WHERE autopilot_id = $1
|
|
ORDER BY created_at DESC
|
|
LIMIT $2 OFFSET $3
|
|
`
|
|
|
|
type ListAutopilotRunsParams struct {
|
|
AutopilotID pgtype.UUID `json:"autopilot_id"`
|
|
Limit int32 `json:"limit"`
|
|
Offset int32 `json:"offset"`
|
|
}
|
|
|
|
func (q *Queries) ListAutopilotRuns(ctx context.Context, arg ListAutopilotRunsParams) ([]AutopilotRun, error) {
|
|
rows, err := q.db.Query(ctx, listAutopilotRuns, arg.AutopilotID, arg.Limit, arg.Offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []AutopilotRun{}
|
|
for rows.Next() {
|
|
var i AutopilotRun
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.TriggerID,
|
|
&i.Source,
|
|
&i.Status,
|
|
&i.IssueID,
|
|
&i.TaskID,
|
|
&i.TriggeredAt,
|
|
&i.CompletedAt,
|
|
&i.FailureReason,
|
|
&i.TriggerPayload,
|
|
&i.Result,
|
|
&i.CreatedAt,
|
|
&i.SquadID,
|
|
&i.PlannedAt,
|
|
&i.WebhookDeliveryID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAutopilotSubscribers = `-- name: ListAutopilotSubscribers :many
|
|
|
|
SELECT autopilot_id, user_type, user_id, created_at FROM autopilot_subscriber
|
|
WHERE autopilot_id = $1
|
|
ORDER BY created_at ASC, user_id ASC
|
|
`
|
|
|
|
// =====================
|
|
// Autopilot Subscribers
|
|
// =====================
|
|
// ORDER BY created_at keeps chip rendering stable across refreshes.
|
|
func (q *Queries) ListAutopilotSubscribers(ctx context.Context, autopilotID pgtype.UUID) ([]AutopilotSubscriber, error) {
|
|
rows, err := q.db.Query(ctx, listAutopilotSubscribers, autopilotID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []AutopilotSubscriber{}
|
|
for rows.Next() {
|
|
var i AutopilotSubscriber
|
|
if err := rows.Scan(
|
|
&i.AutopilotID,
|
|
&i.UserType,
|
|
&i.UserID,
|
|
&i.CreatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAutopilotTriggers = `-- name: ListAutopilotTriggers :many
|
|
|
|
SELECT id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters, published_by_type, published_by_id FROM autopilot_trigger
|
|
WHERE autopilot_id = $1
|
|
ORDER BY created_at ASC
|
|
`
|
|
|
|
// =====================
|
|
// Autopilot Trigger CRUD
|
|
// =====================
|
|
func (q *Queries) ListAutopilotTriggers(ctx context.Context, autopilotID pgtype.UUID) ([]AutopilotTrigger, error) {
|
|
rows, err := q.db.Query(ctx, listAutopilotTriggers, autopilotID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []AutopilotTrigger{}
|
|
for rows.Next() {
|
|
var i AutopilotTrigger
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.Kind,
|
|
&i.Enabled,
|
|
&i.CronExpression,
|
|
&i.Timezone,
|
|
&i.NextRunAt,
|
|
&i.WebhookToken,
|
|
&i.Label,
|
|
&i.LastFiredAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Provider,
|
|
&i.SigningSecret,
|
|
&i.EventFilters,
|
|
&i.PublishedByType,
|
|
&i.PublishedByID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAutopilots = `-- name: ListAutopilots :many
|
|
|
|
SELECT
|
|
a.id, a.workspace_id, a.title, a.description, a.assignee_id, a.status, a.execution_mode, a.issue_title_template, a.created_by_type, a.created_by_id, a.last_run_at, a.created_at, a.updated_at, a.assignee_type, a.project_id,
|
|
(
|
|
SELECT array_agg(DISTINCT t.kind ORDER BY t.kind)
|
|
FROM autopilot_trigger t
|
|
WHERE t.autopilot_id = a.id AND t.enabled
|
|
)::text[] AS trigger_kinds,
|
|
(
|
|
SELECT min(t.next_run_at)
|
|
FROM autopilot_trigger t
|
|
WHERE t.autopilot_id = a.id AND t.enabled AND t.kind = 'schedule'
|
|
)::timestamptz AS next_run_at,
|
|
COALESCE((
|
|
SELECT r.status
|
|
FROM autopilot_run r
|
|
WHERE r.autopilot_id = a.id
|
|
ORDER BY r.triggered_at DESC
|
|
LIMIT 1
|
|
), '')::text AS last_run_status
|
|
FROM autopilot a
|
|
WHERE a.workspace_id = $1
|
|
AND (
|
|
($2::text IS NULL AND a.status <> 'archived')
|
|
OR a.status = $2
|
|
)
|
|
ORDER BY a.created_at DESC
|
|
`
|
|
|
|
type ListAutopilotsParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Status pgtype.Text `json:"status"`
|
|
}
|
|
|
|
type ListAutopilotsRow struct {
|
|
Autopilot Autopilot `json:"autopilot"`
|
|
TriggerKinds []string `json:"trigger_kinds"`
|
|
NextRunAt pgtype.Timestamptz `json:"next_run_at"`
|
|
LastRunStatus string `json:"last_run_status"`
|
|
}
|
|
|
|
// =====================
|
|
// Autopilot CRUD
|
|
// =====================
|
|
// List rows carry three derived columns the list UI needs (trigger badges,
|
|
// next run, last-run outcome) so the page never has to N+1 into the detail
|
|
// endpoint. trigger_kinds/next_run_at only consider ENABLED triggers — the
|
|
// columns answer "how does this fire today", not "what is configured".
|
|
// last_run_status is COALESCEd to ” (never ran) because sqlc cannot infer
|
|
// nullability through a scalar subquery; the handler maps ” back to omitted.
|
|
func (q *Queries) ListAutopilots(ctx context.Context, arg ListAutopilotsParams) ([]ListAutopilotsRow, error) {
|
|
rows, err := q.db.Query(ctx, listAutopilots, arg.WorkspaceID, arg.Status)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListAutopilotsRow{}
|
|
for rows.Next() {
|
|
var i ListAutopilotsRow
|
|
if err := rows.Scan(
|
|
&i.Autopilot.ID,
|
|
&i.Autopilot.WorkspaceID,
|
|
&i.Autopilot.Title,
|
|
&i.Autopilot.Description,
|
|
&i.Autopilot.AssigneeID,
|
|
&i.Autopilot.Status,
|
|
&i.Autopilot.ExecutionMode,
|
|
&i.Autopilot.IssueTitleTemplate,
|
|
&i.Autopilot.CreatedByType,
|
|
&i.Autopilot.CreatedByID,
|
|
&i.Autopilot.LastRunAt,
|
|
&i.Autopilot.CreatedAt,
|
|
&i.Autopilot.UpdatedAt,
|
|
&i.Autopilot.AssigneeType,
|
|
&i.Autopilot.ProjectID,
|
|
&i.TriggerKinds,
|
|
&i.NextRunAt,
|
|
&i.LastRunStatus,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listSchedulableAutopilotTriggers = `-- name: ListSchedulableAutopilotTriggers :many
|
|
|
|
SELECT t.id, t.autopilot_id, t.cron_expression, t.timezone, t.created_at, t.last_fired_at
|
|
FROM autopilot_trigger t
|
|
JOIN autopilot a ON a.id = t.autopilot_id
|
|
WHERE t.kind = 'schedule'
|
|
AND t.enabled = TRUE
|
|
AND a.status = 'active'
|
|
AND t.cron_expression IS NOT NULL
|
|
AND t.cron_expression <> ''
|
|
ORDER BY t.id
|
|
`
|
|
|
|
type ListSchedulableAutopilotTriggersRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
AutopilotID pgtype.UUID `json:"autopilot_id"`
|
|
CronExpression pgtype.Text `json:"cron_expression"`
|
|
Timezone pgtype.Text `json:"timezone"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
LastFiredAt pgtype.Timestamptz `json:"last_fired_at"`
|
|
}
|
|
|
|
// =====================
|
|
// Scheduler Queries
|
|
// =====================
|
|
// Lists every schedule trigger the autopilot_schedule_dispatch JobSpec
|
|
// should consider this tick. Returns just the columns the scheduler's
|
|
// scope provider + PlansForScope hook need; the full trigger row is
|
|
// re-loaded by the handler so a trigger update between scope-list and
|
|
// handler-run sees the latest enabled / cron values.
|
|
//
|
|
// last_fired_at is read so the planner hook can anchor cold-start
|
|
// enumeration on the most recent successful fire (set by either the
|
|
// legacy goroutine before the new scheduler took over, or the new
|
|
// scheduler's own post-dispatch advance — AdvanceTriggerNextRun, falling
|
|
// back to TouchAutopilotTriggerFiredAt on a cron parse error). Without it,
|
|
// a trigger that was created days ago and fired by the legacy code
|
|
// looks like a brand-new trigger to the new scheduler on first tick
|
|
// and the half-open `(created_at, now]` enumeration replays the most
|
|
// recent already-fired occurrence — exactly the post-deploy
|
|
// spurious-fire reported on MUL-3551 dev.
|
|
//
|
|
// Filters out webhook / api triggers, disabled triggers, paused/archived
|
|
// autopilots, and any trigger missing its cron expression. ORDER BY id
|
|
// keeps the per-tick scope list stable across replicas.
|
|
func (q *Queries) ListSchedulableAutopilotTriggers(ctx context.Context) ([]ListSchedulableAutopilotTriggersRow, error) {
|
|
rows, err := q.db.Query(ctx, listSchedulableAutopilotTriggers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListSchedulableAutopilotTriggersRow{}
|
|
for rows.Next() {
|
|
var i ListSchedulableAutopilotTriggersRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.CronExpression,
|
|
&i.Timezone,
|
|
&i.CreatedAt,
|
|
&i.LastFiredAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const recoverPartialAutopilotRun = `-- name: RecoverPartialAutopilotRun :exec
|
|
UPDATE autopilot_run
|
|
SET status = 'failed',
|
|
completed_at = now(),
|
|
failure_reason = 'recovered partial dispatch (crashed before downstream creation)',
|
|
planned_at = NULL
|
|
WHERE id = $1
|
|
`
|
|
|
|
// Recovers a partial-state autopilot_run from a crashed first attempt
|
|
// (the runner wrote the run row but died before creating the downstream
|
|
// issue/task) so that a subsequent DispatchAutopilotForPlan call can
|
|
// create a fresh run at the same (trigger_id, planned_at).
|
|
//
|
|
// Setting planned_at = NULL clears the partial-unique slot held by
|
|
// uq_autopilot_run_trigger_planned, letting the new INSERT proceed.
|
|
// The row stays in autopilot_run as a FAILED record (with a recovery
|
|
// reason) so ops still see the abandoned attempt in the run history —
|
|
// it is not silently deleted.
|
|
func (q *Queries) RecoverPartialAutopilotRun(ctx context.Context, id pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, recoverPartialAutopilotRun, id)
|
|
return err
|
|
}
|
|
|
|
const rotateAutopilotTriggerWebhookToken = `-- name: RotateAutopilotTriggerWebhookToken :one
|
|
UPDATE autopilot_trigger
|
|
SET webhook_token = $2,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
AND kind = 'webhook'
|
|
RETURNING id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters, published_by_type, published_by_id
|
|
`
|
|
|
|
type RotateAutopilotTriggerWebhookTokenParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WebhookToken pgtype.Text `json:"webhook_token"`
|
|
}
|
|
|
|
// Rotates the bearer token for a webhook trigger. Restricted to kind='webhook'
|
|
// so an accidental call against a schedule/api trigger is a no-op (returns no
|
|
// rows) rather than corrupting unrelated state.
|
|
func (q *Queries) RotateAutopilotTriggerWebhookToken(ctx context.Context, arg RotateAutopilotTriggerWebhookTokenParams) (AutopilotTrigger, error) {
|
|
row := q.db.QueryRow(ctx, rotateAutopilotTriggerWebhookToken, arg.ID, arg.WebhookToken)
|
|
var i AutopilotTrigger
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.Kind,
|
|
&i.Enabled,
|
|
&i.CronExpression,
|
|
&i.Timezone,
|
|
&i.NextRunAt,
|
|
&i.WebhookToken,
|
|
&i.Label,
|
|
&i.LastFiredAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Provider,
|
|
&i.SigningSecret,
|
|
&i.EventFilters,
|
|
&i.PublishedByType,
|
|
&i.PublishedByID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const selectAutopilotsExceedingFailureThreshold = `-- name: SelectAutopilotsExceedingFailureThreshold :many
|
|
|
|
WITH stats AS (
|
|
SELECT autopilot_id,
|
|
count(*) FILTER (WHERE status IN ('completed', 'failed')) AS total,
|
|
count(*) FILTER (WHERE status = 'failed') AS failed
|
|
FROM autopilot_run
|
|
WHERE created_at >= $3::timestamptz
|
|
GROUP BY autopilot_id
|
|
)
|
|
SELECT a.id, a.workspace_id, a.title, a.assignee_id,
|
|
a.created_by_type, a.created_by_id,
|
|
s.total::bigint AS total_runs,
|
|
s.failed::bigint AS failed_runs
|
|
FROM autopilot a
|
|
JOIN stats s ON s.autopilot_id = a.id
|
|
WHERE a.status = 'active'
|
|
AND s.total >= $1::bigint
|
|
AND s.failed::float8 / NULLIF(s.total, 0)::float8 >= $2::float8
|
|
ORDER BY s.failed DESC, a.id ASC
|
|
`
|
|
|
|
type SelectAutopilotsExceedingFailureThresholdParams struct {
|
|
MinRuns int64 `json:"min_runs"`
|
|
FailRatioThreshold float64 `json:"fail_ratio_threshold"`
|
|
Since pgtype.Timestamptz `json:"since"`
|
|
}
|
|
|
|
type SelectAutopilotsExceedingFailureThresholdRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Title string `json:"title"`
|
|
AssigneeID pgtype.UUID `json:"assignee_id"`
|
|
CreatedByType string `json:"created_by_type"`
|
|
CreatedByID pgtype.UUID `json:"created_by_id"`
|
|
TotalRuns int64 `json:"total_runs"`
|
|
FailedRuns int64 `json:"failed_runs"`
|
|
}
|
|
|
|
// =====================
|
|
// Failure-rate auto-pause
|
|
// =====================
|
|
// Find active autopilots whose recent run failure rate exceeds the threshold.
|
|
// Counts only "real" terminal runs (completed | failed). 'skipped' is
|
|
// excluded from BOTH numerator and denominator: an admission-skipped run
|
|
// (e.g. assignee runtime offline at dispatch time, MUL-1899) is neither a
|
|
// success nor a failure, so it must not dilute the failure ratio (which
|
|
// would let a 100%-failing autopilot mask itself behind a wall of skips)
|
|
// nor inflate it. issue_created/running are still excluded so in-flight
|
|
// work isn't penalised.
|
|
// Used by the failure monitor to auto-pause sustained-failure autopilots
|
|
// (the canonical example from MUL-1336 was an autopilot scheduled every 5 min
|
|
// that 100% failed for days, burning ~1.5k useless tasks per week).
|
|
func (q *Queries) SelectAutopilotsExceedingFailureThreshold(ctx context.Context, arg SelectAutopilotsExceedingFailureThresholdParams) ([]SelectAutopilotsExceedingFailureThresholdRow, error) {
|
|
rows, err := q.db.Query(ctx, selectAutopilotsExceedingFailureThreshold, arg.MinRuns, arg.FailRatioThreshold, arg.Since)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []SelectAutopilotsExceedingFailureThresholdRow{}
|
|
for rows.Next() {
|
|
var i SelectAutopilotsExceedingFailureThresholdRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.AssigneeID,
|
|
&i.CreatedByType,
|
|
&i.CreatedByID,
|
|
&i.TotalRuns,
|
|
&i.FailedRuns,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const setAutopilotTriggerPublisher = `-- name: SetAutopilotTriggerPublisher :exec
|
|
UPDATE autopilot_trigger
|
|
SET published_by_type = $2, published_by_id = $3, updated_at = now()
|
|
WHERE id = $1
|
|
`
|
|
|
|
type SetAutopilotTriggerPublisherParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
PublishedByType pgtype.Text `json:"published_by_type"`
|
|
PublishedByID pgtype.UUID `json:"published_by_id"`
|
|
}
|
|
|
|
// Re-stamp a single trigger's responsible publisher after a substantive edit of
|
|
// THAT trigger (cron / filter / enabled / webhook security). Future runs it fires
|
|
// become accountable to this member (MUL-4302 trigger_owner transfer).
|
|
func (q *Queries) SetAutopilotTriggerPublisher(ctx context.Context, arg SetAutopilotTriggerPublisherParams) error {
|
|
_, err := q.db.Exec(ctx, setAutopilotTriggerPublisher, arg.ID, arg.PublishedByType, arg.PublishedByID)
|
|
return err
|
|
}
|
|
|
|
const setAutopilotTriggerPublishersByAutopilot = `-- name: SetAutopilotTriggerPublishersByAutopilot :exec
|
|
UPDATE autopilot_trigger
|
|
SET published_by_type = $2, published_by_id = $3, updated_at = now()
|
|
WHERE autopilot_id = $1
|
|
`
|
|
|
|
type SetAutopilotTriggerPublishersByAutopilotParams struct {
|
|
AutopilotID pgtype.UUID `json:"autopilot_id"`
|
|
PublishedByType pgtype.Text `json:"published_by_type"`
|
|
PublishedByID pgtype.UUID `json:"published_by_id"`
|
|
}
|
|
|
|
// Re-stamp ALL of an autopilot's triggers' responsible publisher after a substantive
|
|
// AUTOPILOT-level edit (target / instructions / assignee / execution-mode / enable).
|
|
// Such a change governs every trigger's future runs, so responsibility transfers to
|
|
// the editing member for all of them; a per-trigger edit uses the single-trigger
|
|
// variant so it never reassigns another trigger (MUL-4302).
|
|
func (q *Queries) SetAutopilotTriggerPublishersByAutopilot(ctx context.Context, arg SetAutopilotTriggerPublishersByAutopilotParams) error {
|
|
_, err := q.db.Exec(ctx, setAutopilotTriggerPublishersByAutopilot, arg.AutopilotID, arg.PublishedByType, arg.PublishedByID)
|
|
return err
|
|
}
|
|
|
|
const setAutopilotTriggerSigningSecret = `-- name: SetAutopilotTriggerSigningSecret :one
|
|
UPDATE autopilot_trigger
|
|
SET signing_secret = $2,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
AND kind = 'webhook'
|
|
RETURNING id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters, published_by_type, published_by_id
|
|
`
|
|
|
|
type SetAutopilotTriggerSigningSecretParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
SigningSecret pgtype.Text `json:"signing_secret"`
|
|
}
|
|
|
|
// Writes the signing secret for a webhook trigger. Kept as a dedicated query
|
|
// (not a field on UpdateAutopilotTrigger) so the request body for the
|
|
// write-only endpoint only ever carries the secret value, with no risk of an
|
|
// accidental log line leaking it alongside other fields. Restricted to
|
|
// webhook triggers to avoid corrupting unrelated state.
|
|
func (q *Queries) SetAutopilotTriggerSigningSecret(ctx context.Context, arg SetAutopilotTriggerSigningSecretParams) (AutopilotTrigger, error) {
|
|
row := q.db.QueryRow(ctx, setAutopilotTriggerSigningSecret, arg.ID, arg.SigningSecret)
|
|
var i AutopilotTrigger
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.Kind,
|
|
&i.Enabled,
|
|
&i.CronExpression,
|
|
&i.Timezone,
|
|
&i.NextRunAt,
|
|
&i.WebhookToken,
|
|
&i.Label,
|
|
&i.LastFiredAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Provider,
|
|
&i.SigningSecret,
|
|
&i.EventFilters,
|
|
&i.PublishedByType,
|
|
&i.PublishedByID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const setAutopilotTriggerWebhookToken = `-- name: SetAutopilotTriggerWebhookToken :one
|
|
UPDATE autopilot_trigger
|
|
SET webhook_token = $2,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters, published_by_type, published_by_id
|
|
`
|
|
|
|
type SetAutopilotTriggerWebhookTokenParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WebhookToken pgtype.Text `json:"webhook_token"`
|
|
}
|
|
|
|
// Sets the webhook token at creation time. CreateAutopilotTrigger inserts the
|
|
// row first (using its full 8-arg signature), then this query attaches the
|
|
// token. Splitting the create + token-set keeps the existing CreateAutopilotTrigger
|
|
// query usable by the schedule path without forcing every caller to think
|
|
// about webhook_token.
|
|
func (q *Queries) SetAutopilotTriggerWebhookToken(ctx context.Context, arg SetAutopilotTriggerWebhookTokenParams) (AutopilotTrigger, error) {
|
|
row := q.db.QueryRow(ctx, setAutopilotTriggerWebhookToken, arg.ID, arg.WebhookToken)
|
|
var i AutopilotTrigger
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.Kind,
|
|
&i.Enabled,
|
|
&i.CronExpression,
|
|
&i.Timezone,
|
|
&i.NextRunAt,
|
|
&i.WebhookToken,
|
|
&i.Label,
|
|
&i.LastFiredAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Provider,
|
|
&i.SigningSecret,
|
|
&i.EventFilters,
|
|
&i.PublishedByType,
|
|
&i.PublishedByID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const systemPauseAutopilot = `-- name: SystemPauseAutopilot :one
|
|
UPDATE autopilot
|
|
SET status = 'paused', updated_at = now()
|
|
WHERE id = $1 AND status = 'active'
|
|
RETURNING id, workspace_id, title, description, assignee_id, status, execution_mode, issue_title_template, created_by_type, created_by_id, last_run_at, created_at, updated_at, assignee_type, project_id
|
|
`
|
|
|
|
// Atomically pauses an autopilot only if it is currently active. Returns no
|
|
// rows when the autopilot was already paused/archived (or another worker
|
|
// raced first), letting the caller treat that as a benign no-op rather than
|
|
// an error.
|
|
func (q *Queries) SystemPauseAutopilot(ctx context.Context, id pgtype.UUID) (Autopilot, error) {
|
|
row := q.db.QueryRow(ctx, systemPauseAutopilot, id)
|
|
var i Autopilot
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.AssigneeID,
|
|
&i.Status,
|
|
&i.ExecutionMode,
|
|
&i.IssueTitleTemplate,
|
|
&i.CreatedByType,
|
|
&i.CreatedByID,
|
|
&i.LastRunAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.AssigneeType,
|
|
&i.ProjectID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const touchAutopilotTriggerFiredAt = `-- name: TouchAutopilotTriggerFiredAt :exec
|
|
UPDATE autopilot_trigger
|
|
SET last_fired_at = now(),
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
`
|
|
|
|
// Bumps last_fired_at after a webhook fires, regardless of whether the
|
|
// dispatch succeeded, was admission-skipped, or even if Autopilot status
|
|
// transitioned to paused/disabled at exactly the wrong moment. Disabled /
|
|
// paused early-return paths in the handler never call this.
|
|
func (q *Queries) TouchAutopilotTriggerFiredAt(ctx context.Context, id pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, touchAutopilotTriggerFiredAt, id)
|
|
return err
|
|
}
|
|
|
|
const updateAutopilot = `-- name: UpdateAutopilot :one
|
|
UPDATE autopilot SET
|
|
title = COALESCE($2, title),
|
|
description = COALESCE($3, description),
|
|
assignee_type = COALESCE($4, assignee_type),
|
|
assignee_id = COALESCE($5::uuid, assignee_id),
|
|
status = COALESCE($6, status),
|
|
execution_mode = COALESCE($7, execution_mode),
|
|
issue_title_template = $8,
|
|
project_id = $9,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING id, workspace_id, title, description, assignee_id, status, execution_mode, issue_title_template, created_by_type, created_by_id, last_run_at, created_at, updated_at, assignee_type, project_id
|
|
`
|
|
|
|
type UpdateAutopilotParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Title pgtype.Text `json:"title"`
|
|
Description pgtype.Text `json:"description"`
|
|
AssigneeType pgtype.Text `json:"assignee_type"`
|
|
AssigneeID pgtype.UUID `json:"assignee_id"`
|
|
Status pgtype.Text `json:"status"`
|
|
ExecutionMode pgtype.Text `json:"execution_mode"`
|
|
IssueTitleTemplate pgtype.Text `json:"issue_title_template"`
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
}
|
|
|
|
func (q *Queries) UpdateAutopilot(ctx context.Context, arg UpdateAutopilotParams) (Autopilot, error) {
|
|
row := q.db.QueryRow(ctx, updateAutopilot,
|
|
arg.ID,
|
|
arg.Title,
|
|
arg.Description,
|
|
arg.AssigneeType,
|
|
arg.AssigneeID,
|
|
arg.Status,
|
|
arg.ExecutionMode,
|
|
arg.IssueTitleTemplate,
|
|
arg.ProjectID,
|
|
)
|
|
var i Autopilot
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.AssigneeID,
|
|
&i.Status,
|
|
&i.ExecutionMode,
|
|
&i.IssueTitleTemplate,
|
|
&i.CreatedByType,
|
|
&i.CreatedByID,
|
|
&i.LastRunAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.AssigneeType,
|
|
&i.ProjectID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateAutopilotLastRunAt = `-- name: UpdateAutopilotLastRunAt :exec
|
|
UPDATE autopilot SET last_run_at = now(), updated_at = now()
|
|
WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) UpdateAutopilotLastRunAt(ctx context.Context, id pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, updateAutopilotLastRunAt, id)
|
|
return err
|
|
}
|
|
|
|
const updateAutopilotRunCompleted = `-- name: UpdateAutopilotRunCompleted :one
|
|
UPDATE autopilot_run
|
|
SET status = 'completed', completed_at = now(), result = $2
|
|
WHERE id = $1
|
|
RETURNING id, autopilot_id, trigger_id, source, status, issue_id, task_id, triggered_at, completed_at, failure_reason, trigger_payload, result, created_at, squad_id, planned_at, webhook_delivery_id
|
|
`
|
|
|
|
type UpdateAutopilotRunCompletedParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Result []byte `json:"result"`
|
|
}
|
|
|
|
func (q *Queries) UpdateAutopilotRunCompleted(ctx context.Context, arg UpdateAutopilotRunCompletedParams) (AutopilotRun, error) {
|
|
row := q.db.QueryRow(ctx, updateAutopilotRunCompleted, arg.ID, arg.Result)
|
|
var i AutopilotRun
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.TriggerID,
|
|
&i.Source,
|
|
&i.Status,
|
|
&i.IssueID,
|
|
&i.TaskID,
|
|
&i.TriggeredAt,
|
|
&i.CompletedAt,
|
|
&i.FailureReason,
|
|
&i.TriggerPayload,
|
|
&i.Result,
|
|
&i.CreatedAt,
|
|
&i.SquadID,
|
|
&i.PlannedAt,
|
|
&i.WebhookDeliveryID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateAutopilotRunFailed = `-- name: UpdateAutopilotRunFailed :one
|
|
UPDATE autopilot_run
|
|
SET status = 'failed', completed_at = now(), failure_reason = $2
|
|
WHERE id = $1
|
|
RETURNING id, autopilot_id, trigger_id, source, status, issue_id, task_id, triggered_at, completed_at, failure_reason, trigger_payload, result, created_at, squad_id, planned_at, webhook_delivery_id
|
|
`
|
|
|
|
type UpdateAutopilotRunFailedParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
FailureReason pgtype.Text `json:"failure_reason"`
|
|
}
|
|
|
|
func (q *Queries) UpdateAutopilotRunFailed(ctx context.Context, arg UpdateAutopilotRunFailedParams) (AutopilotRun, error) {
|
|
row := q.db.QueryRow(ctx, updateAutopilotRunFailed, arg.ID, arg.FailureReason)
|
|
var i AutopilotRun
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.TriggerID,
|
|
&i.Source,
|
|
&i.Status,
|
|
&i.IssueID,
|
|
&i.TaskID,
|
|
&i.TriggeredAt,
|
|
&i.CompletedAt,
|
|
&i.FailureReason,
|
|
&i.TriggerPayload,
|
|
&i.Result,
|
|
&i.CreatedAt,
|
|
&i.SquadID,
|
|
&i.PlannedAt,
|
|
&i.WebhookDeliveryID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateAutopilotRunIssueCreated = `-- name: UpdateAutopilotRunIssueCreated :one
|
|
UPDATE autopilot_run
|
|
SET status = 'issue_created', issue_id = $2
|
|
WHERE id = $1
|
|
RETURNING id, autopilot_id, trigger_id, source, status, issue_id, task_id, triggered_at, completed_at, failure_reason, trigger_payload, result, created_at, squad_id, planned_at, webhook_delivery_id
|
|
`
|
|
|
|
type UpdateAutopilotRunIssueCreatedParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
}
|
|
|
|
func (q *Queries) UpdateAutopilotRunIssueCreated(ctx context.Context, arg UpdateAutopilotRunIssueCreatedParams) (AutopilotRun, error) {
|
|
row := q.db.QueryRow(ctx, updateAutopilotRunIssueCreated, arg.ID, arg.IssueID)
|
|
var i AutopilotRun
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.TriggerID,
|
|
&i.Source,
|
|
&i.Status,
|
|
&i.IssueID,
|
|
&i.TaskID,
|
|
&i.TriggeredAt,
|
|
&i.CompletedAt,
|
|
&i.FailureReason,
|
|
&i.TriggerPayload,
|
|
&i.Result,
|
|
&i.CreatedAt,
|
|
&i.SquadID,
|
|
&i.PlannedAt,
|
|
&i.WebhookDeliveryID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateAutopilotRunRunning = `-- name: UpdateAutopilotRunRunning :one
|
|
UPDATE autopilot_run
|
|
SET status = 'running', task_id = $2
|
|
WHERE id = $1
|
|
RETURNING id, autopilot_id, trigger_id, source, status, issue_id, task_id, triggered_at, completed_at, failure_reason, trigger_payload, result, created_at, squad_id, planned_at, webhook_delivery_id
|
|
`
|
|
|
|
type UpdateAutopilotRunRunningParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
TaskID pgtype.UUID `json:"task_id"`
|
|
}
|
|
|
|
func (q *Queries) UpdateAutopilotRunRunning(ctx context.Context, arg UpdateAutopilotRunRunningParams) (AutopilotRun, error) {
|
|
row := q.db.QueryRow(ctx, updateAutopilotRunRunning, arg.ID, arg.TaskID)
|
|
var i AutopilotRun
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.TriggerID,
|
|
&i.Source,
|
|
&i.Status,
|
|
&i.IssueID,
|
|
&i.TaskID,
|
|
&i.TriggeredAt,
|
|
&i.CompletedAt,
|
|
&i.FailureReason,
|
|
&i.TriggerPayload,
|
|
&i.Result,
|
|
&i.CreatedAt,
|
|
&i.SquadID,
|
|
&i.PlannedAt,
|
|
&i.WebhookDeliveryID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateAutopilotRunSkipped = `-- name: UpdateAutopilotRunSkipped :one
|
|
UPDATE autopilot_run
|
|
SET status = 'skipped', completed_at = now(), failure_reason = $2
|
|
WHERE id = $1
|
|
RETURNING id, autopilot_id, trigger_id, source, status, issue_id, task_id, triggered_at, completed_at, failure_reason, trigger_payload, result, created_at, squad_id, planned_at, webhook_delivery_id
|
|
`
|
|
|
|
type UpdateAutopilotRunSkippedParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
FailureReason pgtype.Text `json:"failure_reason"`
|
|
}
|
|
|
|
// Marks an autopilot_run as skipped without enqueueing any task. Used by the
|
|
// pre-flight admission check when the assignee agent's runtime is offline:
|
|
// creating an issue / task in that state would just pile a doomed job onto
|
|
// agent_task_queue (the canonical "持续给离线 local agent 入队" symptom from
|
|
// MUL-1899). Recording the skip + reason gives the UI / failure monitor / ops
|
|
// a paper trail without polluting the failure ratio.
|
|
func (q *Queries) UpdateAutopilotRunSkipped(ctx context.Context, arg UpdateAutopilotRunSkippedParams) (AutopilotRun, error) {
|
|
row := q.db.QueryRow(ctx, updateAutopilotRunSkipped, arg.ID, arg.FailureReason)
|
|
var i AutopilotRun
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.TriggerID,
|
|
&i.Source,
|
|
&i.Status,
|
|
&i.IssueID,
|
|
&i.TaskID,
|
|
&i.TriggeredAt,
|
|
&i.CompletedAt,
|
|
&i.FailureReason,
|
|
&i.TriggerPayload,
|
|
&i.Result,
|
|
&i.CreatedAt,
|
|
&i.SquadID,
|
|
&i.PlannedAt,
|
|
&i.WebhookDeliveryID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateAutopilotRunSkippedWithResult = `-- name: UpdateAutopilotRunSkippedWithResult :one
|
|
UPDATE autopilot_run
|
|
SET status = 'skipped',
|
|
completed_at = now(),
|
|
failure_reason = $2,
|
|
result = $3
|
|
WHERE id = $1
|
|
RETURNING id, autopilot_id, trigger_id, source, status, issue_id, task_id, triggered_at, completed_at, failure_reason, trigger_payload, result, created_at, squad_id, planned_at, webhook_delivery_id
|
|
`
|
|
|
|
type UpdateAutopilotRunSkippedWithResultParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
FailureReason pgtype.Text `json:"failure_reason"`
|
|
Result []byte `json:"result"`
|
|
}
|
|
|
|
func (q *Queries) UpdateAutopilotRunSkippedWithResult(ctx context.Context, arg UpdateAutopilotRunSkippedWithResultParams) (AutopilotRun, error) {
|
|
row := q.db.QueryRow(ctx, updateAutopilotRunSkippedWithResult, arg.ID, arg.FailureReason, arg.Result)
|
|
var i AutopilotRun
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.TriggerID,
|
|
&i.Source,
|
|
&i.Status,
|
|
&i.IssueID,
|
|
&i.TaskID,
|
|
&i.TriggeredAt,
|
|
&i.CompletedAt,
|
|
&i.FailureReason,
|
|
&i.TriggerPayload,
|
|
&i.Result,
|
|
&i.CreatedAt,
|
|
&i.SquadID,
|
|
&i.PlannedAt,
|
|
&i.WebhookDeliveryID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateAutopilotTrigger = `-- name: UpdateAutopilotTrigger :one
|
|
UPDATE autopilot_trigger SET
|
|
enabled = COALESCE($2::boolean, enabled),
|
|
cron_expression = COALESCE($3, cron_expression),
|
|
timezone = COALESCE($4, timezone),
|
|
next_run_at = $5,
|
|
label = COALESCE($6, label),
|
|
event_filters = COALESCE($7, event_filters),
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at, provider, signing_secret, event_filters, published_by_type, published_by_id
|
|
`
|
|
|
|
type UpdateAutopilotTriggerParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Enabled pgtype.Bool `json:"enabled"`
|
|
CronExpression pgtype.Text `json:"cron_expression"`
|
|
Timezone pgtype.Text `json:"timezone"`
|
|
NextRunAt pgtype.Timestamptz `json:"next_run_at"`
|
|
Label pgtype.Text `json:"label"`
|
|
EventFilters []byte `json:"event_filters"`
|
|
}
|
|
|
|
func (q *Queries) UpdateAutopilotTrigger(ctx context.Context, arg UpdateAutopilotTriggerParams) (AutopilotTrigger, error) {
|
|
row := q.db.QueryRow(ctx, updateAutopilotTrigger,
|
|
arg.ID,
|
|
arg.Enabled,
|
|
arg.CronExpression,
|
|
arg.Timezone,
|
|
arg.NextRunAt,
|
|
arg.Label,
|
|
arg.EventFilters,
|
|
)
|
|
var i AutopilotTrigger
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.AutopilotID,
|
|
&i.Kind,
|
|
&i.Enabled,
|
|
&i.CronExpression,
|
|
&i.Timezone,
|
|
&i.NextRunAt,
|
|
&i.WebhookToken,
|
|
&i.Label,
|
|
&i.LastFiredAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Provider,
|
|
&i.SigningSecret,
|
|
&i.EventFilters,
|
|
&i.PublishedByType,
|
|
&i.PublishedByID,
|
|
)
|
|
return i, err
|
|
}
|