mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-12 12:18:55 +02:00
* feat(server): orphan-task recovery + auto-retry + manual rerun (MUL-1128)
When the daemon process crashed mid-task the issue was stuck at
in_progress for up to 2.5h: the in-flight task timeout was the only
mechanism that ever moved the row, and the runtime heartbeat sweeper
only fires after the runtime stays offline for 45s — a quick restart
beats both windows.
This change implements the A+B plan from the issue thread:
A. lifecycle hygiene
- migration 055 adds attempt / max_attempts / parent_task_id /
failure_reason / last_heartbeat_at to agent_task_queue
- new daemon-auth endpoint POST /runtimes/{id}/recover-orphans:
daemon calls it on every register so the server fails any
dispatched/running tasks the previous process left behind
- new daemon-auth endpoint POST /tasks/{id}/session: persists the
agent's session_id + work_dir mid-flight so a crash doesn't
lose the resume pointer (claude+codex emit MessageStatus with
SessionID; daemon forwards on the first one it sees)
- FailAgentTask / FailStaleTasks / FailTasksForOfflineRuntimes
now set failure_reason ('agent_error' / 'timeout' /
'runtime_offline')
B. auto-retry with resume context
- TaskService.MaybeRetryFailedTask spawns a fresh queued attempt
carrying parent's session_id/work_dir when the failure reason
is infrastructure-shaped (timeout, runtime_offline,
runtime_recovery) and attempt < max_attempts; skips autopilot
- wired into the runtime sweeper paths and TaskService.FailTask
so the user transparently sees a new in_progress run instead of
a stuck row
- new user-auth POST /api/issues/{id}/rerun + multica issue rerun
CLI for the manual escape hatch
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(server): address PR review for orphan-task recovery (MUL-1128)
Three review-must-fix items on top of the A+B implementation:
1. recover-orphans now funnels through TaskService.HandleFailedTasks,
the same shared post-failure pipeline used by the runtime sweeper.
This guarantees task:failed events are emitted, agent status is
reconciled, and issues stuck in_progress with no remaining active
task are reset to todo even when no auto-retry is created
(max_attempts exhausted, autopilot, non-retryable reason).
2. RerunIssue now uses CancelAgentTasksByIssueAndAgent, scoped to the
issue's current assignee. The previous implementation called
CancelAgentTasksByIssue, which would collateral-cancel parallel
@-mention agents on the same issue.
3. GetLastTaskSession now considers both completed and failed tasks
(mirroring GetLastChatTaskSession), ordering by the most recent
timestamp. With UpdateAgentTaskSession pinning session_id/work_dir
mid-flight, an auto-retry or manual rerun of a daemon-crash failure
now actually resumes the prior conversation context instead of
starting fresh — matching the stated B-branch behaviour.
go build / go vet pass; the existing service and agent test suites pass.
runtime_sweeper / handler integration tests require a local DB with the
055 migration (and the pre-existing 050 first_executed_at column).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
946 lines
26 KiB
Go
946 lines
26 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.30.0
|
|
// source: autopilot.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
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 claimDueScheduleTriggers = `-- name: ClaimDueScheduleTriggers :many
|
|
|
|
UPDATE autopilot_trigger t
|
|
SET next_run_at = NULL
|
|
FROM autopilot a
|
|
WHERE t.autopilot_id = a.id
|
|
AND t.kind = 'schedule'
|
|
AND t.enabled = true
|
|
AND t.next_run_at IS NOT NULL
|
|
AND t.next_run_at <= now()
|
|
AND a.status = 'active'
|
|
RETURNING 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, a.workspace_id AS autopilot_workspace_id
|
|
`
|
|
|
|
type ClaimDueScheduleTriggersRow 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"`
|
|
AutopilotWorkspaceID pgtype.UUID `json:"autopilot_workspace_id"`
|
|
}
|
|
|
|
// =====================
|
|
// Scheduler Queries
|
|
// =====================
|
|
// Atomically claim all due schedule triggers to prevent concurrent execution.
|
|
// Joins the autopilot table to ensure only active autopilots are fired.
|
|
func (q *Queries) ClaimDueScheduleTriggers(ctx context.Context) ([]ClaimDueScheduleTriggersRow, error) {
|
|
rows, err := q.db.Query(ctx, claimDueScheduleTriggers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ClaimDueScheduleTriggersRow{}
|
|
for rows.Next() {
|
|
var i ClaimDueScheduleTriggersRow
|
|
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.AutopilotWorkspaceID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const createAutopilot = `-- name: CreateAutopilot :one
|
|
INSERT INTO autopilot (
|
|
workspace_id, project_id, title, description, assignee_id,
|
|
priority, status, execution_mode, issue_title_template,
|
|
created_by_type, created_by_id
|
|
) VALUES (
|
|
$1, $9, $2, $10, $3,
|
|
$4, $5, $6, $11,
|
|
$7, $8
|
|
) RETURNING id, workspace_id, project_id, title, description, assignee_id, priority, status, execution_mode, issue_title_template, created_by_type, created_by_id, last_run_at, created_at, updated_at
|
|
`
|
|
|
|
type CreateAutopilotParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Title string `json:"title"`
|
|
AssigneeID pgtype.UUID `json:"assignee_id"`
|
|
Priority string `json:"priority"`
|
|
Status string `json:"status"`
|
|
ExecutionMode string `json:"execution_mode"`
|
|
CreatedByType string `json:"created_by_type"`
|
|
CreatedByID pgtype.UUID `json:"created_by_id"`
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
Description pgtype.Text `json:"description"`
|
|
IssueTitleTemplate pgtype.Text `json:"issue_title_template"`
|
|
}
|
|
|
|
func (q *Queries) CreateAutopilot(ctx context.Context, arg CreateAutopilotParams) (Autopilot, error) {
|
|
row := q.db.QueryRow(ctx, createAutopilot,
|
|
arg.WorkspaceID,
|
|
arg.Title,
|
|
arg.AssigneeID,
|
|
arg.Priority,
|
|
arg.Status,
|
|
arg.ExecutionMode,
|
|
arg.CreatedByType,
|
|
arg.CreatedByID,
|
|
arg.ProjectID,
|
|
arg.Description,
|
|
arg.IssueTitleTemplate,
|
|
)
|
|
var i Autopilot
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.ProjectID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.AssigneeID,
|
|
&i.Priority,
|
|
&i.Status,
|
|
&i.ExecutionMode,
|
|
&i.IssueTitleTemplate,
|
|
&i.CreatedByType,
|
|
&i.CreatedByID,
|
|
&i.LastRunAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createAutopilotRun = `-- name: CreateAutopilotRun :one
|
|
|
|
INSERT INTO autopilot_run (
|
|
autopilot_id, trigger_id, source, status, trigger_payload
|
|
) VALUES (
|
|
$1, $4, $2, $3, $5
|
|
) RETURNING id, autopilot_id, trigger_id, source, status, issue_id, task_id, triggered_at, completed_at, failure_reason, trigger_payload, result, created_at
|
|
`
|
|
|
|
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"`
|
|
}
|
|
|
|
// =====================
|
|
// Autopilot Run Management
|
|
// =====================
|
|
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,
|
|
)
|
|
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,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createAutopilotTask = `-- name: CreateAutopilotTask :one
|
|
|
|
INSERT INTO agent_task_queue (agent_id, runtime_id, issue_id, status, priority, autopilot_run_id)
|
|
VALUES ($1, $2, NULL, 'queued', $3, $4)
|
|
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, last_heartbeat_at
|
|
`
|
|
|
|
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"`
|
|
}
|
|
|
|
// =====================
|
|
// Task Queue (run_only mode)
|
|
// =====================
|
|
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,
|
|
)
|
|
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.LastHeartbeatAt,
|
|
)
|
|
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
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5,
|
|
$6, $7, $8
|
|
) RETURNING id, autopilot_id, kind, enabled, cron_expression, timezone, next_run_at, webhook_token, label, last_fired_at, created_at, updated_at
|
|
`
|
|
|
|
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"`
|
|
}
|
|
|
|
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,
|
|
)
|
|
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,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteAutopilot = `-- name: DeleteAutopilot :exec
|
|
DELETE FROM autopilot WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteAutopilot(ctx context.Context, id pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteAutopilot, id)
|
|
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 getAutopilot = `-- name: GetAutopilot :one
|
|
SELECT id, workspace_id, project_id, title, description, assignee_id, priority, status, execution_mode, issue_title_template, created_by_type, created_by_id, last_run_at, created_at, updated_at 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.ProjectID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.AssigneeID,
|
|
&i.Priority,
|
|
&i.Status,
|
|
&i.ExecutionMode,
|
|
&i.IssueTitleTemplate,
|
|
&i.CreatedByType,
|
|
&i.CreatedByID,
|
|
&i.LastRunAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getAutopilotInWorkspace = `-- name: GetAutopilotInWorkspace :one
|
|
SELECT id, workspace_id, project_id, title, description, assignee_id, priority, status, execution_mode, issue_title_template, created_by_type, created_by_id, last_run_at, created_at, updated_at 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.ProjectID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.AssigneeID,
|
|
&i.Priority,
|
|
&i.Status,
|
|
&i.ExecutionMode,
|
|
&i.IssueTitleTemplate,
|
|
&i.CreatedByType,
|
|
&i.CreatedByID,
|
|
&i.LastRunAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
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 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,
|
|
)
|
|
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 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,
|
|
)
|
|
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 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,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
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 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,
|
|
); 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 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,
|
|
); 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 id, workspace_id, project_id, title, description, assignee_id, priority, status, execution_mode, issue_title_template, created_by_type, created_by_id, last_run_at, created_at, updated_at FROM autopilot
|
|
WHERE workspace_id = $1
|
|
AND ($2::text IS NULL OR status = $2)
|
|
ORDER BY created_at DESC
|
|
`
|
|
|
|
type ListAutopilotsParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Status pgtype.Text `json:"status"`
|
|
}
|
|
|
|
// =====================
|
|
// Autopilot CRUD
|
|
// =====================
|
|
func (q *Queries) ListAutopilots(ctx context.Context, arg ListAutopilotsParams) ([]Autopilot, error) {
|
|
rows, err := q.db.Query(ctx, listAutopilots, arg.WorkspaceID, arg.Status)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Autopilot{}
|
|
for rows.Next() {
|
|
var i Autopilot
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.ProjectID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.AssigneeID,
|
|
&i.Priority,
|
|
&i.Status,
|
|
&i.ExecutionMode,
|
|
&i.IssueTitleTemplate,
|
|
&i.CreatedByType,
|
|
&i.CreatedByID,
|
|
&i.LastRunAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const recoverLostTriggers = `-- name: RecoverLostTriggers :many
|
|
|
|
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, a.workspace_id AS autopilot_workspace_id
|
|
FROM autopilot_trigger t
|
|
JOIN autopilot a ON t.autopilot_id = a.id
|
|
WHERE t.kind = 'schedule'
|
|
AND t.enabled = true
|
|
AND t.next_run_at IS NULL
|
|
AND t.cron_expression IS NOT NULL
|
|
AND a.status = 'active'
|
|
`
|
|
|
|
type RecoverLostTriggersRow 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"`
|
|
AutopilotWorkspaceID pgtype.UUID `json:"autopilot_workspace_id"`
|
|
}
|
|
|
|
// =====================
|
|
// Scheduler Recovery
|
|
// =====================
|
|
// Finds schedule triggers that were claimed (next_run_at = NULL) but never
|
|
// advanced — typically due to a scheduler crash. Returns them so the scheduler
|
|
// can recompute next_run_at.
|
|
func (q *Queries) RecoverLostTriggers(ctx context.Context) ([]RecoverLostTriggersRow, error) {
|
|
rows, err := q.db.Query(ctx, recoverLostTriggers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []RecoverLostTriggersRow{}
|
|
for rows.Next() {
|
|
var i RecoverLostTriggersRow
|
|
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.AutopilotWorkspaceID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const updateAutopilot = `-- name: UpdateAutopilot :one
|
|
UPDATE autopilot SET
|
|
title = COALESCE($2, title),
|
|
description = COALESCE($3, description),
|
|
assignee_id = COALESCE($4::uuid, assignee_id),
|
|
project_id = $5,
|
|
priority = COALESCE($6, priority),
|
|
status = COALESCE($7, status),
|
|
execution_mode = COALESCE($8, execution_mode),
|
|
issue_title_template = $9,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING id, workspace_id, project_id, title, description, assignee_id, priority, status, execution_mode, issue_title_template, created_by_type, created_by_id, last_run_at, created_at, updated_at
|
|
`
|
|
|
|
type UpdateAutopilotParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Title pgtype.Text `json:"title"`
|
|
Description pgtype.Text `json:"description"`
|
|
AssigneeID pgtype.UUID `json:"assignee_id"`
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
Priority pgtype.Text `json:"priority"`
|
|
Status pgtype.Text `json:"status"`
|
|
ExecutionMode pgtype.Text `json:"execution_mode"`
|
|
IssueTitleTemplate pgtype.Text `json:"issue_title_template"`
|
|
}
|
|
|
|
func (q *Queries) UpdateAutopilot(ctx context.Context, arg UpdateAutopilotParams) (Autopilot, error) {
|
|
row := q.db.QueryRow(ctx, updateAutopilot,
|
|
arg.ID,
|
|
arg.Title,
|
|
arg.Description,
|
|
arg.AssigneeID,
|
|
arg.ProjectID,
|
|
arg.Priority,
|
|
arg.Status,
|
|
arg.ExecutionMode,
|
|
arg.IssueTitleTemplate,
|
|
)
|
|
var i Autopilot
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.ProjectID,
|
|
&i.Title,
|
|
&i.Description,
|
|
&i.AssigneeID,
|
|
&i.Priority,
|
|
&i.Status,
|
|
&i.ExecutionMode,
|
|
&i.IssueTitleTemplate,
|
|
&i.CreatedByType,
|
|
&i.CreatedByID,
|
|
&i.LastRunAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
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
|
|
`
|
|
|
|
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,
|
|
)
|
|
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
|
|
`
|
|
|
|
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,
|
|
)
|
|
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
|
|
`
|
|
|
|
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,
|
|
)
|
|
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
|
|
`
|
|
|
|
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,
|
|
)
|
|
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),
|
|
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
|
|
`
|
|
|
|
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"`
|
|
}
|
|
|
|
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,
|
|
)
|
|
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,
|
|
)
|
|
return i, err
|
|
}
|