mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 21:39:54 +02:00
* refactor(views): migrate agent/runtime/skill lists to TanStack DataTable
Replace the per-page CSS Grid + minmax(min, fr) + sticky-first-col + truncate
implementation with a TanStack Table backend rendered through a Dice UI-style
DataTable shell. Column widths are now px-based via column.size, so cells
no longer shrink or auto-truncate as the viewport narrows; when the sum of
columns exceeds the viewport, the container scrolls horizontally instead.
- Add @tanstack/react-table to the catalog (8.21.3) and wire it into
packages/ui (dep) and packages/views (peerDep).
- packages/ui: new DataTable + DataTableColumnHeader + lib/data-table.ts
(getColumnPinningStyle), adapted from Dice UI's registry. The shell
renders <table> directly (skipping shadcn's <Table> wrapper) so its own
outer overflow controls both axes — no nested overflow conflicts.
- packages/views: each list now declares ColumnDef[] with explicit
cell renderers. Row click navigates to detail via onRowClick (instead of
wrapping <tr> in <a>, which is invalid HTML); kebab dropdowns
stopPropagation so they don't trigger the row navigation.
- Drop the previous AGENT_LIST_GRID / GRID_WITH_OWNER / ROW_GRID
templates and the sticky-first-col / subgrid mechanics that came with
them. agent-list-item.tsx is removed; runtime-list.tsx and
skills-page.tsx are trimmed to thin wrappers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agent): cap description at 255 chars (db + api + ui)
Symmetric enforcement across DB, server, and UI:
- Migration 060: pre-flight truncate of any oversize rows, then ADD
CONSTRAINT NOT VALID + VALIDATE CONSTRAINT so the new check doesn't
block writes during validation.
- Server handler validates utf8.RuneCountInString on Create/Update and
rejects over-limit input with 400.
- Front-end gets AGENT_DESCRIPTION_MAX_LENGTH in core/agents/constants
(single source of truth shared by the create dialog + edit modal +
test suite) and a CharCounter component that warns at 90% and errors
past the cap.
- Description editor moves from a 288px popover to a roomy modal.
Editor body is mounted only while the dialog is open, so the local
draft state is locked in at mount time and never reset by an external
WS update — the React-recommended replacement for the
useEffect(reset, [value]) anti-pattern.
Counted in code points everywhere (rune count / spread length /
char_length) so multibyte input agrees across all three layers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(views): data-table polish across runtime + skill lists
Builds on the DataTable migration in 2be0f287:
- Add ColumnMeta.grow flag — declared via TanStack module augmentation
in ui/lib/data-table.ts. Columns marked meta.grow skip their inline
width so fixed table-layout assigns them the leftover container space
(no spacer column). The Title-grows / others-fixed pattern from
Linear / GitHub PR rows.
- Authoritative table min-width = sum of column.size, applied to the
<table> itself (fixed-layout ignores cell-level min-width per spec,
so the floor has to live on the table).
- Header tightens to h-8 + uppercase + tracking-wider; pinned cells
switch to opaque bg + group-hover so they cover content scrolling
beneath them and follow row hover state.
- Toolbar slot removed from DataTable (callers wrap the toolbar
themselves now — keeps DataTable single-purpose).
Also: hover-card popup stops contextmenu / auxclick / dblclick from
bubbling out (in addition to click). Stops the popup from triggering
ancestor handlers (e.g. issue list rows) on right-click / middle-click
without breaking Base UI's outside-click dismiss, which listens to
pointerdown — pointerdown is deliberately NOT stopped.
Runtime + skill list pages updated to use the new sizing model.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(agent): drop LastTaskState, introduce 3-state Workload
Continues the presence-model rework started in #1794 / #1798.
The previous LastTaskState union (running / completed / failed /
cancelled / idle) carried historical outcome at the list level — a
runtime-healthy agent whose last task failed showed a sticky red dot
indistinguishable from a daemon-dead agent.
New model: presence is two orthogonal "right-now" dimensions:
AgentAvailability — runtime reachability only (online / unstable /
offline). Drives the dot colour everywhere.
Workload — current load (working / queued / idle). Three
states, never historical. Failure / completion /
cancellation are surfaced via Recent Work + Inbox,
not list-level state.
`queued` (= nothing running, ≥1 queued) is an honest "stuck on offline
runtime" signal. To avoid amber flashes during the brief enqueue→claim
race on healthy runtimes, the queued chip composes with availability:
muted on online, warning amber otherwise.
Activity tab cleanup that follows from the new model:
- failureReasonLabel relocated from agents/presence.ts to
tabs/task-failure.ts (presence no longer owns historical state).
- Recent Work paginates (5 initial, +20 per "Show more"); chat-session
tasks are filtered out of every Agent-scoped surface to keep
"team work" separate from private chat.
- Agents page drops the lastTaskFilter chip group; users find broken
agents via Inbox / Recent Work, not a list-level filter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(task): trigger summary snapshot + task:queued lifecycle event
Two task-lifecycle improvements that ship together because they share
the same enqueue/retry hot paths and changes interleave inside task.go:
1. trigger_summary snapshot (migration 061)
New nullable column on agent_task_queue. Comment-triggered tasks
snapshot the comment content; autopilot tasks snapshot the run title.
Truncated to 200 runes via strings.Builder so multibyte input counts
correctly without O(N²) concatenation. Snapshot survives source
edits/deletes — every task row self-describes across surfaces (issue
detail Execution log, agent activity tooltip, inbox) without joining
back to the originating row.
Retry rows inherit the parent's snapshot (CreateRetryTask SELECT) so
the description stays meaningful across attempts. The UI is
responsible for stacking "Retry #N" context on top.
2. task:queued WS event
New protocol event covering the ∅ → queued transition. Front-end
types/events.ts registers it; use-realtime-sync's task: prefix path
already invalidates task caches via onAny, so old clients without
this exact-match subscription still refresh correctly. Specific
subscribers (sticky banner) get sub-second updates instead of
waiting for daemon claim.
Retry path now broadcasts task:queued (not task:dispatch) — same
status transition shape as enqueue, so all "new task created" paths
agree on one event type.
Ordering: broadcastTaskEvent runs *before* notifyTaskAvailable so
the queued event is published into the WS bus before the daemon is
poked. Without this, a fast daemon could claim and emit task:dispatch
over the wire before the in-process queued broadcast fan-out reached
clients — race window is tiny but unsafe-by-construction.
Per-agent task list (agentTasksKeys.all) and per-issue task list
(["issues","tasks"]) added to the task: invalidation set so Activity
tab Recent Work and the Execution log section stay fresh.
Type contracts: AgentTask gains parent_task_id / attempt /
trigger_comment_id (already returned by the API, just missing from TS)
plus the new trigger_summary field.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(issue): ExecutionLogSection — unified active+past runs panel
Replaces two pieces:
- the click-to-expand timeline that lived inside AgentLiveCard
- the standalone TaskRunHistory below the main content
with a single right-panel section that lists every agent run for the
issue. Active runs sit at the top (always visible when present); past
runs collapse behind a "Show past runs (N)" toggle, sorted failed →
cancelled → completed within group.
Active rows show the trigger summary, status + relative time, and
Cancel / Transcript actions on hover (gradient backdrop fades the
status text rather than hard-clipping). Past rows show the same
shape minus Cancel.
Retry tasks prepend "Retry #N · " to the inherited summary so they're
distinguishable from their parent (which would otherwise share the
exact same trigger text).
Cache key registered as issueKeys.tasks(issueId); the global
useRealtimeSync task: prefix path already invalidates ["issues","tasks"]
on every task lifecycle event, so the section stays fresh without
local WS subscriptions.
AgentLiveCard slims down to a header-only "agent is working" sticky
banner — keeps the at-a-glance "is anyone working on this right now"
signal and the Stop / Transcript actions, drops the inline timeline
that ExecutionLogSection now owns. Subscribes to both task:queued and
task:dispatch so retries (which only emit queued) land in the banner
without waiting for daemon claim.
issue-detail mounts ExecutionLogSection in the right panel and removes
the now-defunct TaskRunHistory call site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
929 lines
26 KiB
Go
929 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, title, description, assignee_id,
|
|
status, execution_mode, issue_title_template,
|
|
created_by_type, created_by_id
|
|
) VALUES (
|
|
$1, $2, $8, $3,
|
|
$4, $5, $9,
|
|
$6, $7
|
|
) 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
|
|
`
|
|
|
|
type CreateAutopilotParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Title string `json:"title"`
|
|
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"`
|
|
}
|
|
|
|
func (q *Queries) CreateAutopilot(ctx context.Context, arg CreateAutopilotParams) (Autopilot, error) {
|
|
row := q.db.QueryRow(ctx, createAutopilot,
|
|
arg.WorkspaceID,
|
|
arg.Title,
|
|
arg.AssigneeID,
|
|
arg.Status,
|
|
arg.ExecutionMode,
|
|
arg.CreatedByType,
|
|
arg.CreatedByID,
|
|
arg.Description,
|
|
arg.IssueTitleTemplate,
|
|
)
|
|
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,
|
|
)
|
|
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, trigger_summary)
|
|
VALUES ($1, $2, NULL, 'queued', $3, $4, $5)
|
|
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, trigger_summary
|
|
`
|
|
|
|
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"`
|
|
}
|
|
|
|
// =====================
|
|
// 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,
|
|
arg.TriggerSummary,
|
|
)
|
|
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,
|
|
&i.TriggerSummary,
|
|
)
|
|
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, title, description, assignee_id, 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.Title,
|
|
&i.Description,
|
|
&i.AssigneeID,
|
|
&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, title, description, assignee_id, 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.Title,
|
|
&i.Description,
|
|
&i.AssigneeID,
|
|
&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, title, description, assignee_id, 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.Title,
|
|
&i.Description,
|
|
&i.AssigneeID,
|
|
&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),
|
|
status = COALESCE($5, status),
|
|
execution_mode = COALESCE($6, execution_mode),
|
|
issue_title_template = $7,
|
|
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
|
|
`
|
|
|
|
type UpdateAutopilotParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Title pgtype.Text `json:"title"`
|
|
Description pgtype.Text `json:"description"`
|
|
AssigneeID pgtype.UUID `json:"assignee_id"`
|
|
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.Status,
|
|
arg.ExecutionMode,
|
|
arg.IssueTitleTemplate,
|
|
)
|
|
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,
|
|
)
|
|
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
|
|
}
|