Files
multica/server/pkg/db/generated/autopilot.sql.go
T
f39debf8d1 MUL-7344: skip the issue read and the comment scan when the server already answered them (#8377)
* feat(daemon): read the comment delta with one --since, not a roots scan (MUL-7344)

A resumed follow-up turn whose claim carried a server-computed issue-wide
comment delta was still told to run the workflow's two-step scan: a
`--roots-only --summary` pass to discover which threads moved, then a
`--thread ... --since ...` expansion per thread. That rediscovers, in two or
more calls, an answer the server already has.

`--since <anchor>` without `--thread` IS that answer.
ListCommentsSinceForIssue returns every comment on the issue created after the
anchor, in every thread and at every nesting level, with no folding, on the
same `created_at > since` predicate CountNewCommentsSince counted with. It is a
strict superset of what the roots scan could surface: roots-only
`last_activity_at` is MAX(created_at) over a thread, so any comment able to
move a thread's activity is already in the `--since` result.

So BuildNewCommentsHint now hands over exactly one wide read and says it
answers step 2's scan — the same rule BuildResumedCommentsHint already applies
to a server-computed EMPTY delta. The optional second command stays a THREAD
read on `--tail 30` for the reply itself; it is never the wide read, and it
never combines `--thread` with `--since`, which drops the thread root.

Brief step 2 and the multica-platform skill's bounded-reads rule both gain the
matching static sentence, so an agent is not left choosing between the hint and
two surfaces that contradict it.

Cold start, dropped resume and unknown-delta paths are untouched, byte for
byte: only an affirmative server report may stand in for the scan.

Co-authored-by: multica-agent <[email protected]>

* feat(claim): compare the issue against the last run so a resumed turn can skip the read (MUL-7344)

A follow-up turn that resumes its provider session already holds the issue it
read minutes ago, and was still told, unconditionally, to run `multica issue
get` before doing anything. On an issue that did not move, that read returns
bytes the session already has.

The claim now answers the question instead. Each claim records the comparable
state of the issue it built its payload from — status, assignee, priority, and
sha256 of title and description — in a new `agent_task_queue.issue_snapshot`
JSONB column, and compares against the snapshot the SAME agent's previous run
on the SAME issue recorded. The response carries `issue_state_delta_known`,
`issue_changed_fields`, and the current `issue_status` / assignee.

Why a per-claim snapshot and not a `content_revision` on `issue`: a revision
column has to be bumped by every writer that touches one of those five fields,
and the first one missed reports "unchanged" forever with no symptom. A
snapshot taken at claim time has exactly one writer. It also rides the anchor
the comment delta already uses (agent_id, issue_id, started_at DESC), so both
deltas on a claim mean "since your last run" in the same sense.

The daemon renders one of three things, gated on a REAL resume:

  - unchanged  → reported as workflow step 1's answer; the read survives only
                 as "if resumed memory is not enough".
  - changed    → the changed field NAMES plus the read, so the agent knows what
                 to look for instead of diffing blind.
  - otherwise  → the pre-existing instruction, byte for byte.

`issue_state_delta_known` carries the same contract as
`new_comments_delta_known` (MUL-6984), and for the same reason: a cold start,
a missing prior snapshot, a read error, a foreign shape version and an old
server all produce an empty changed-field list, identical to a genuine
"nothing changed". Only the flag separates them, and only the affirmative one
may stand in for the read.

The compared set is named in full in the prompt rather than summarised as "the
issue": labels, parent, due date, stage, project and metadata are NOT compared,
and an agent told "unchanged" has to be able to see that.

The snapshot write is NOT gated on the task being comment-backed — an
assignment run that recorded nothing would leave the next run with no baseline.
It shares the receipt's CAS, so a stale handler cannot write over a newer
reclaim or after execution starts, and a snapshot that fails to encode is
simply not recorded: the next run reports "not compared" and reads the issue.

Co-authored-by: multica-agent <[email protected]>

* fix(claim): renumber the migration past main and fold in review nits (MUL-7344)

CI caught a real collision: main landed 471_comment_deleted_at through
474 while this branch was open, so 471_agent_task_issue_snapshot shared a
numeric prefix and TestMigrationNumericPrefixesAreUnique failed. Renumbered to
475 after merging main; no other migration references it.

Review nits from the PR review, all non-blocking, all cheap to take now:

1. Brief step 2's closing sentence told the agent that a resumed run's scan
   surfaces moved threads via `last_activity_at`. That is now true only on the
   path where the delta is UNKNOWN — when the server hands over a `--since`
   delta there is no scan to read it from. Rewritten to attach the
   `last_activity_at` guidance to the scan itself, wherever the scan happens.
   Done in this PR rather than deferred because this PR already moves brief
   bytes, so it costs no extra prompt-cache invalidation.

2. The two per-claim anchor reads are now one. GetLastTaskStartedAtForIssueAndAgent
   and GetLastTaskIssueSnapshotForIssueAndAgent selected the same row with the
   same WHERE and ORDER, differing only in projection; they are replaced by
   GetLastRunAnchorForIssueAndAgent returning started_at and issue_snapshot
   together. A comment-triggered claim is back to ONE anchor read, as before
   this branch.

   The saved round trip is the smaller half. The real gain is that "since your
   last run" is now a single resolved fact on a claim instead of two
   independently-resolved ones that could disagree. The shared read also shares
   a failure mode, and both consumers already degrade the same safe way: comment
   scan required, issue comparison unknown.

3. The hint's `%d new comment(s)` excludes the injected trigger and the agent's
   own comments; the `--since` read it hands over has no such filter, so the row
   count is legitimately higher. Narrowing the read to match would mean a second
   count-shaped query for no gain, so the text now states the mismatch and its
   direction outright instead of leaving the agent to reconcile two numbers.

New test pins the merge: one claim, one prior run, both deltas resolved from
that single anchor row.

Co-authored-by: multica-agent <[email protected]>

* fix(migrations): renumber the issue-snapshot migration to 478 (MUL-7344)

main landed 475_issue_status_key_not_reserved through 477 while the previous
renumber was in flight, so 475_agent_task_issue_snapshot collided in turn.
Merged main again and moved to 478. TestMigrationNumericPrefixesAreUnique
passes, and the full ledger applies cleanly from scratch through 478.

Co-authored-by: multica-agent <[email protected]>

* fix(claim): measure both deltas from the run whose session is resumed (MUL-7344)

Review found a real correctness bug, not a performance one: the claim could tell
a resumed agent "the issue is unchanged" while the session it handed back
predated the change.

Both deltas were anchored on the most recently STARTED run for (agent, issue).
The session the daemon resumes is chosen by a different rule, and the two are
not the same row:

  - GetLastTaskSession skips poisoned and retired sessions, so a newer run that
    died on an unprocessable conversation is passed over for an OLDER healthy
    one.
  - A manual rerun resumes the exact source task the operator clicked.

So: an old session remembers description A, the user edits it to B, a newer run
records B and then dies unresumable. The next follow-up resumes the OLD session
but compares B against B, reports "unchanged", and the agent keeps working from
a context that still says A. PriorSessionResumeUnavailable does not catch this —
it flags a missing Codex rollout, which is only one of the ways the resumed
session can be older than the newest run.

The fix binds both deltas to the run actually being resumed. GetLastTaskSession
now returns that row's started_at and issue_snapshot alongside its session, the
rerun path takes them from its source task, and the delta computation moves
after resume resolution so it can only ever read the adopted row. Where no
session is adopted — no prior session, a session on another runtime, a
forced-fresh retry, a rerun whose source failure poisoned the conversation — no
delta is reported and the daemon performs the reads it always has.

That makes the standalone anchor query unnecessary: the anchor is now a
projection on a row the claim already fetched, so a claim reads no extra row at
all (GetLastRunAnchorForIssueAndAgent is removed).

The precondition is now visible in the fixtures: a prior run has to be
resumable for a delta to exist, so the comment-delta tests seed a session_id.
The regression test covers both paths and was verified to fail against the
previous anchor — both sub-cases report an empty changed-field list, which is
exactly the wrong "unchanged".

Also renumbers the migration 478 → 491; main landed 478_issue_status_category_expand
through 490 while the branch was open.

Co-authored-by: multica-agent <[email protected]>

* fix(claim): a never-started anchor row may date neither delta (MUL-7344)

Final review found the last corner of the resumed-run bug. Binding the anchor to
the resumed SESSION was necessary but not sufficient: GetLastTaskSession returns
that session's latest TERMINAL row, and that row is not always a run that ran.

A retry child can inherit the session, be claimed — which writes its snapshot —
and then fail before the agent ever starts: runtime offline during prepare,
cancelled while dispatched. Its snapshot describes the issue as of ITS claim,
while the session's memory stops at the last run that actually executed. If the
description was edited in between, the current issue matches that snapshot and
the claim reports "unchanged" to an agent whose context predates the edit.

The started_at gate now covers both deltas rather than only the comment one.
The comment delta was already immune because it read StartedAt directly; the
issue delta read only the snapshot, so it had no such guard. Hoisting the check
makes the row's requirement explicit — a row that never ran dates nothing — and
the inner duplicate is dropped so no reader has to wonder what each guards.

Regression test reproduces the exact fixture and was verified to fail without
the gate: known=true with an empty changed-field list, i.e. the false
"unchanged".

Also from review:

- Adopt the no-adopted-session table (missing session, session on another
  runtime, force-fresh retry, poisoned rerun source). Each seeds a usable
  snapshot AND an unread comment, so a regression has something to report and
  the assertion cannot pass on an empty fixture.
- The resumed-run regression test asserted a comment anchor that never
  executed: both sub-cases created only the trigger comment, which the count
  excludes, so NewCommentsSince was empty and the branch was dead. A comment
  between the two runs now makes it run and pins known/count/since.
- Rewrite the migration header, which still described the old
  "(agent_id, issue_id, started_at DESC)" anchor and an index rationale for a
  column that is never a lookup key.

Co-authored-by: multica-agent <[email protected]>

* refactor(claim): compare title, description and status only (MUL-7344)

The snapshot compared five fields. The comparison answers exactly one question
— must the agent run `issue get` again? — so a field earns a place only if
changing it alters what the agent does AND its current value is not already in
the per-turn message.

Assignee fails the second test: the claim ships the current assignee outright,
and "is this mine now" is answered by that value, not by whether it moved.
Priority fails the first: it is reachable only by reading the issue, but it does
not change what the agent does. Both are dropped.

Status stays even though it is also shipped as a current value, so comparing it
is strictly redundant for "what is it now". It earns its place as a signal
rather than a value: "changed: status" is the clearest evidence that somebody
intervened between the runs — a push back from in_review to todo means the
delivery was rejected — and it costs nothing to carry.

Narrowing the set means a narrower claim, so the prompt now says "compared
title, description and status". An issue whose only change is assignee or
priority is reported as unchanged, which is correct and now pinned by a test
that also asserts the current assignee still ships — without that second
assertion the fixture's UPDATE could silently not apply and the test would pass
on nothing.

Snapshot version goes to 2, because the rule is that the version tracks the
compared SET. No deployment has written a v1 row, but the gate is what makes
narrowing safe in general: a v1 row read by v2 code is "not compared", which
degrades to the unconditional read.

Test fixtures now build snapshots at issueSnapshotVersion rather than a literal
1, so a future bump cannot quietly turn them into not-compared tests while they
keep passing. The foreign-version case stays explicit at +1.

Also renumbers the migration 491 -> 495; main landed 491 through 494 while the
branch was open. Fifth renumber.

Co-authored-by: multica-agent <[email protected]>

* refactor(claim): keep the snapshot at v1 (MUL-7344)

The previous commit bumped issueSnapshotVersion to 2 when it narrowed the
compared set. That was the version rule applied past its purpose.

The rule — "bump when the compared SET changes" — exists to stop a stored
snapshot from being compared against a differently-shaped one. It protects
DEPLOYED rows. This column is introduced by this same unmerged PR: the
migration exists only on this branch, main has neither the column nor the
writer, and no environment has ever stored a row with the wider set. There is
nothing for the bump to protect, and shipping v2 as the first version the
column has ever held would leave a reader hunting for a v1 that never existed.

Back to 1. The `v` field and its unknown-version gate stay exactly as they
were — those are what make a real narrowing safe later. The comment now says
which version this is and why the review-time narrowing did not earn a bump,
so the rule still has a stated precedent without a burned number.

No behaviour change: the compared set is still title, description and status,
and fixtures build at issueSnapshotVersion rather than a literal, so they
followed the constant back down.

Co-authored-by: multica-agent <[email protected]>

* fix(claim): only a completed run may date a delta (MUL-7344)

Final review found a window one step later than the never-started case.

A snapshot is written at CLAIM time. The daemon then writes started_at
(TaskService.StartTask) and only afterwards launches the provider. So a run can
carry both a snapshot and a started_at while its prompt never reached the
session: the daemon exits during prepare, the runtime drops, orphan recovery
marks the row failed. Anchoring there compares the current issue against a
snapshot describing an edit that session never saw — reporting "unchanged"
across a real change, and dating the comment delta late enough to hide every
comment older than that row's start. Reproduced on the real
CreateRetryTask -> claim -> StartTask -> RecoverOrphanedTasksForRuntime ->
follow-up claim path.

Nothing on the row separates "failed after the provider ran" from "failed
before it ran". session_id is inherited by CreateRetryTask at insert, so it does
not mean the session was fed; started_at precedes the launch, so it does not
mean the turn began. The only terminal status that PROVES delivery is
'completed' — a run cannot finish a turn it never started.

So failed and cancelled sources now date neither delta. Their SESSION is still
resumed exactly as before, and GetLastTaskSession still accepts them on purpose;
those runs simply perform the context reads they always did. That is the cheap
side of the trade: a follow-up after a failure or a stop is already recovering
from something, while the case the optimization exists for — an ordinary
follow-up after a healthy run — is untouched.

Both conditions now live in newResumedRunAnchor rather than at the use site, so
"which run may date a delta" is answered once, and GetLastTaskSession carries
the status needed to answer it.

Regression test is the reviewer's repro, verified to fail without the rule:
known=true with an empty changed list AND a computed zero comment count, i.e.
both a false "unchanged" and a false "no new comments".

Also:
- Renumber the migration 495 -> 499; main landed 495 through 498. Sixth
  renumber.
- The migration's content_revision rationale named the old five-field set while
  arguing about hypothetical issue-row writers; it now refers to "any compared
  field", which is what the argument actually depends on.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-16 16:11:16 +08:00

2633 lines
88 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', pause_reason = NULL, 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, pause_reason
`
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,
&i.PauseReason,
)
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, quota_reservation_id, reason_code, id
) VALUES (
$1, $4, $2, $3, $5,
$6, $7,
$8, $9,
$10, COALESCE($11::uuid, gen_random_uuid())
) 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, quota_reservation_id, reason_code
`
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"`
QuotaReservationID pgtype.UUID `json:"quota_reservation_id"`
ReasonCode pgtype.Text `json:"reason_code"`
ID pgtype.UUID `json:"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,
arg.QuotaReservationID,
arg.ReasonCode,
arg.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,
&i.QuotaReservationID,
&i.ReasonCode,
)
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,
id
)
SELECT
$1, $2, NULL, 'queued', $3, $4, $5,
$6,
$7,
$8,
$9,
$10,
$11,
COALESCE($12::uuid, gen_random_uuid())
WHERE lock_task_owner_rows($1, NULL, $2)
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, trigger_summary, force_fresh_session, is_leader_task, wait_reason, initiator_user_id, handoff_note, prepare_lease_expires_at, squad_id, runtime_mcp_overlay, escalation_for_task_id, fire_at, originator_user_id, runtime_connected_apps, coalesced_comment_ids, delivered_comment_ids, chat_input_task_id, chat_finalize_deferred_at, originator_source, delegated_from_task_id, retry_of_task_id, rerun_of_task_id, rule_version_id, trigger_evidence_kind, trigger_evidence_ref_id, accountable_user_id, session_rollout_missing, retired_session_id, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot
`
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"`
ID pgtype.UUID `json:"id"`
}
// =====================
// Task Queue (run_only mode)
// =====================
// Fenced against workspace teardown: lock_task_owner_rows (migration 284)
// locks the owners' workspace rows in the writer's own transaction and returns
// false once they are gone, so this statement writes no row instead of stranding
// a task in a workspace that has just been deleted (MUL-5999).
// 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,
arg.ID,
)
var i AgentTaskQueue
err := row.Scan(
&i.ID,
&i.AgentID,
&i.IssueID,
&i.Status,
&i.Priority,
&i.DispatchedAt,
&i.StartedAt,
&i.CompletedAt,
&i.Result,
&i.Error,
&i.CreatedAt,
&i.Context,
&i.RuntimeID,
&i.SessionID,
&i.WorkDir,
&i.TriggerCommentID,
&i.ChatSessionID,
&i.AutopilotRunID,
&i.Attempt,
&i.MaxAttempts,
&i.ParentTaskID,
&i.FailureReason,
&i.TriggerSummary,
&i.ForceFreshSession,
&i.IsLeaderTask,
&i.WaitReason,
&i.InitiatorUserID,
&i.HandoffNote,
&i.PrepareLeaseExpiresAt,
&i.SquadID,
&i.RuntimeMcpOverlay,
&i.EscalationForTaskID,
&i.FireAt,
&i.OriginatorUserID,
&i.RuntimeConnectedApps,
&i.CoalescedCommentIds,
&i.DeliveredCommentIds,
&i.ChatInputTaskID,
&i.ChatFinalizeDeferredAt,
&i.OriginatorSource,
&i.DelegatedFromTaskID,
&i.RetryOfTaskID,
&i.RerunOfTaskID,
&i.RuleVersionID,
&i.TriggerEvidenceKind,
&i.TriggerEvidenceRefID,
&i.AccountableUserID,
&i.SessionRolloutMissing,
&i.RetiredSessionID,
&i.QuickActionsDisabled,
&i.RegenerateQuickActionsFor,
&i.BranchName,
&i.DurableWorkDir,
&i.ChannelContextRevision,
&i.CommentThreadID,
&i.CancelledByType,
&i.CancelledByID,
&i.CancelledByName,
&i.IssueSnapshot,
)
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,
created_by_type, created_by_id
) VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8,
COALESCE($9::text, 'generic'),
$10,
$11, $12,
$13, $14
) 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, created_by_type, created_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"`
CreatedByType pgtype.Text `json:"created_by_type"`
CreatedByID pgtype.UUID `json:"created_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,
arg.CreatedByType,
arg.CreatedByID,
)
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,
&i.CreatedByType,
&i.CreatedByID,
)
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 deleteAutopilotSubscribersByMember = `-- name: DeleteAutopilotSubscribersByMember :exec
DELETE FROM autopilot_subscriber AS s
USING autopilot AS a
WHERE s.autopilot_id = a.id
AND a.workspace_id = $1
AND s.user_type = 'member'
AND s.user_id = $2
`
type DeleteAutopilotSubscribersByMemberParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
UserID pgtype.UUID `json:"user_id"`
}
// Autopilot subscribers carry no FK, so member removal must prune them in the
// same application transaction. Scope through the autopilot's workspace: the
// same user may remain subscribed to autopilots in another workspace.
func (q *Queries) DeleteAutopilotSubscribersByMember(ctx context.Context, arg DeleteAutopilotSubscribersByMemberParams) error {
_, err := q.db.Exec(ctx, deleteAutopilotSubscribersByMember, arg.WorkspaceID, arg.UserID)
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 :many
WITH updated_runs AS (
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')
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, quota_reservation_id, reason_code
), locked_reservations AS MATERIALIZED (
SELECT qr.id, qr.workspace_id, qr.period_start, qr.period_end, qr.policy_revision, qr.subscription_version, qr.source, qr.idempotency_key, qr.state, qr.created_at, qr.finalized_at
FROM autopilot_quota_reservation qr
JOIN updated_runs ar ON ar.quota_reservation_id = qr.id
WHERE qr.state = 'reserved'
FOR UPDATE
), released_reservations AS (
UPDATE autopilot_quota_reservation AS qr
SET state = 'released', finalized_at = now()
FROM locked_reservations AS locked
WHERE qr.id = locked.id
AND EXISTS (
SELECT 1 FROM autopilot_quota_period p
WHERE p.workspace_id = locked.workspace_id
AND p.period_start = locked.period_start
AND p.period_end = locked.period_end
)
RETURNING locked.workspace_id, locked.period_start, locked.period_end
), released_by_period AS (
SELECT workspace_id, period_start, period_end, count(*)::bigint AS released_count
FROM released_reservations
GROUP BY workspace_id, period_start, period_end
), settled_periods AS (
UPDATE autopilot_quota_period AS p
SET reserved_count = reserved_count - released.released_count,
updated_at = now()
FROM released_by_period AS released
WHERE p.workspace_id = released.workspace_id
AND p.period_start = released.period_start
AND p.period_end = released.period_end
RETURNING p.workspace_id
)
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, quota_reservation_id, reason_code FROM updated_runs
`
type FailAutopilotRunsByIssueRow struct {
ID pgtype.UUID `json:"id"`
AutopilotID pgtype.UUID `json:"autopilot_id"`
TriggerID pgtype.UUID `json:"trigger_id"`
Source string `json:"source"`
Status string `json:"status"`
IssueID pgtype.UUID `json:"issue_id"`
TaskID pgtype.UUID `json:"task_id"`
TriggeredAt pgtype.Timestamptz `json:"triggered_at"`
CompletedAt pgtype.Timestamptz `json:"completed_at"`
FailureReason pgtype.Text `json:"failure_reason"`
TriggerPayload []byte `json:"trigger_payload"`
Result []byte `json:"result"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
SquadID pgtype.UUID `json:"squad_id"`
PlannedAt pgtype.Timestamptz `json:"planned_at"`
WebhookDeliveryID pgtype.UUID `json:"webhook_delivery_id"`
QuotaReservationID pgtype.UUID `json:"quota_reservation_id"`
ReasonCode pgtype.Text `json:"reason_code"`
}
// Fails active autopilot runs linked to a given issue.
// Must be called BEFORE issue deletion (ON DELETE SET NULL clears issue_id).
// Only still-reserved run_only slots are released. A create_issue slot was
// consumed when the issue was created and remains counted after deletion.
func (q *Queries) FailAutopilotRunsByIssue(ctx context.Context, issueID pgtype.UUID) ([]FailAutopilotRunsByIssueRow, error) {
rows, err := q.db.Query(ctx, failAutopilotRunsByIssue, issueID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []FailAutopilotRunsByIssueRow{}
for rows.Next() {
var i FailAutopilotRunsByIssueRow
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,
&i.QuotaReservationID,
&i.ReasonCode,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
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, pause_reason 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,
&i.PauseReason,
)
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, pause_reason 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,
&i.PauseReason,
)
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, quota_reservation_id, reason_code 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,
&i.QuotaReservationID,
&i.ReasonCode,
)
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, quota_reservation_id, reason_code 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,
&i.QuotaReservationID,
&i.ReasonCode,
)
return i, err
}
const getAutopilotRunByQuotaReservation = `-- name: GetAutopilotRunByQuotaReservation :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, quota_reservation_id, reason_code FROM autopilot_run
WHERE quota_reservation_id = $1
LIMIT 1
`
func (q *Queries) GetAutopilotRunByQuotaReservation(ctx context.Context, quotaReservationID pgtype.UUID) (AutopilotRun, error) {
row := q.db.QueryRow(ctx, getAutopilotRunByQuotaReservation, quotaReservationID)
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,
&i.QuotaReservationID,
&i.ReasonCode,
)
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, quota_reservation_id, reason_code 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,
&i.QuotaReservationID,
&i.ReasonCode,
)
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, quota_reservation_id, reason_code 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,
&i.QuotaReservationID,
&i.ReasonCode,
)
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, quick_actions_disabled, regenerate_quick_actions_for, branch_name, durable_work_dir, channel_context_revision, comment_thread_id, cancelled_by_type, cancelled_by_id, cancelled_by_name, issue_snapshot 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,
&i.QuickActionsDisabled,
&i.RegenerateQuickActionsFor,
&i.BranchName,
&i.DurableWorkDir,
&i.ChannelContextRevision,
&i.CommentThreadID,
&i.CancelledByType,
&i.CancelledByID,
&i.CancelledByName,
&i.IssueSnapshot,
)
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, created_by_type, created_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,
&i.CreatedByType,
&i.CreatedByID,
)
return i, err
}
const getAutopilotTriggerForAutopilot = `-- name: GetAutopilotTriggerForAutopilot :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, t.created_by_type, t.created_by_id FROM autopilot_trigger t
JOIN autopilot a ON a.id = t.autopilot_id
WHERE t.id = $1 AND t.autopilot_id = $2 AND a.workspace_id = $3
`
type GetAutopilotTriggerForAutopilotParams struct {
ID pgtype.UUID `json:"id"`
AutopilotID pgtype.UUID `json:"autopilot_id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
// Trigger lookup BOUND to both the autopilot it must belong to AND that
// autopilot's workspace. Since MUL-6951 the trigger row decides which human a run
// acts as, so an unbound `WHERE id = $1` would let a trigger id from another
// autopilot select the principal. The workspace join closes the other half: the
// caller's membership check proves the resolved human belongs to the workspace it
// passed, not that the AUTOPILOT does, and a member of two workspaces would
// satisfy the former while the trigger came from the other tenant. Callers
// resolving an authorization principal must use this, not GetAutopilotTrigger.
func (q *Queries) GetAutopilotTriggerForAutopilot(ctx context.Context, arg GetAutopilotTriggerForAutopilotParams) (AutopilotTrigger, error) {
row := q.db.QueryRow(ctx, getAutopilotTriggerForAutopilot, arg.ID, arg.AutopilotID, arg.WorkspaceID)
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,
&i.CreatedByType,
&i.CreatedByID,
)
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, t.created_by_type, t.created_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"`
CreatedByType pgtype.Text `json:"created_by_type"`
CreatedByID pgtype.UUID `json:"created_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.CreatedByType,
&i.CreatedByID,
&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, quota_reservation_id, reason_code 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,
&i.QuotaReservationID,
&i.ReasonCode,
); 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 s.autopilot_id, s.user_type, s.user_id, s.created_at FROM autopilot_subscriber AS s
JOIN autopilot AS a ON a.id = s.autopilot_id
JOIN member AS m
ON m.workspace_id = a.workspace_id
AND m.user_id = s.user_id
WHERE s.autopilot_id = $1
AND s.user_type = 'member'
ORDER BY s.created_at ASC, s.user_id ASC
`
// =====================
// Autopilot Subscribers
// =====================
// Only current workspace members are effective subscribers. The membership
// join makes legacy rows left behind by older member-removal code inert on
// both the detail response and the dispatch path, so clients never round-trip
// a hidden departed member into an otherwise valid update.
// 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 listAutopilotSubscribersForAutopilots = `-- name: ListAutopilotSubscribersForAutopilots :many
SELECT s.autopilot_id, s.user_type, s.user_id, s.created_at FROM autopilot_subscriber AS s
JOIN autopilot AS a ON a.id = s.autopilot_id
JOIN member AS m
ON m.workspace_id = a.workspace_id
AND m.user_id = s.user_id
WHERE s.autopilot_id = ANY($1::uuid[])
AND s.user_type = 'member'
ORDER BY s.autopilot_id ASC, s.created_at ASC, s.user_id ASC
`
// Batch form of ListAutopilotSubscribers for the list endpoint, which must not
// issue one query per row. The autopilot_subscriber primary key leads with
// autopilot_id, so ANY($1) is index-supported and no extra index is needed.
// The member join and ordering are identical to the single-autopilot query on
// purpose: list and detail have to agree on who counts as a subscriber, or the
// two projections disagree again (MUL-6680).
func (q *Queries) ListAutopilotSubscribersForAutopilots(ctx context.Context, dollar_1 []pgtype.UUID) ([]AutopilotSubscriber, error) {
rows, err := q.db.Query(ctx, listAutopilotSubscribersForAutopilots, dollar_1)
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, created_by_type, created_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,
&i.CreatedByType,
&i.CreatedByID,
); 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, a.pause_reason,
(
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.Autopilot.PauseReason,
&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 lockAutopilotForUpdate = `-- name: LockAutopilotForUpdate :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, pause_reason FROM autopilot
WHERE id = $1 AND workspace_id = $2
FOR UPDATE
`
type LockAutopilotForUpdateParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
// UpdateAutopilot is a patch assembled from a pre-transaction snapshot. Lock
// and compare that row before applying the patch so a concurrent retarget or
// Runtime teardown cannot be overwritten by stale assignee/status fields.
func (q *Queries) LockAutopilotForUpdate(ctx context.Context, arg LockAutopilotForUpdateParams) (Autopilot, error) {
row := q.db.QueryRow(ctx, lockAutopilotForUpdate, 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,
&i.PauseReason,
)
return i, err
}
const pauseAutopilotsByUnboundAgents = `-- name: PauseAutopilotsByUnboundAgents :many
UPDATE autopilot a
SET status = 'paused',
pause_reason = 'agent_runtime_required',
updated_at = now()
WHERE a.status = 'active'
AND (
(a.assignee_type = 'agent' AND a.assignee_id = ANY($1::uuid[]))
OR (
a.assignee_type = 'squad'
AND EXISTS (
SELECT 1
FROM squad s
WHERE s.id = a.assignee_id
AND s.leader_id = ANY($1::uuid[])
)
)
)
RETURNING 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, a.pause_reason
`
// A runtime delete is a persistent admission failure, not a per-tick event.
// Pause direct-agent automations and squad automations whose leader was
// unbound, preserving the full configuration for an explicit resume after
// rebind. Restrict to active so repeated teardown/retry is idempotent.
func (q *Queries) PauseAutopilotsByUnboundAgents(ctx context.Context, agentIds []pgtype.UUID) ([]Autopilot, error) {
rows, err := q.db.Query(ctx, pauseAutopilotsByUnboundAgents, agentIds)
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,
&i.AssigneeType,
&i.ProjectID,
&i.PauseReason,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const pauseAutopilotsByUnrunnableSquad = `-- name: PauseAutopilotsByUnrunnableSquad :many
UPDATE autopilot
SET status = 'paused',
pause_reason = 'agent_runtime_required',
updated_at = now()
WHERE status = 'active'
AND assignee_type = 'squad'
AND assignee_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, pause_reason
`
// Rotating a squad to an already-unbound leader has the same persistent
// admission failure as Runtime teardown. Pause only automations assigned to
// this squad; direct automations targeting that Agent are unrelated.
func (q *Queries) PauseAutopilotsByUnrunnableSquad(ctx context.Context, squadID pgtype.UUID) ([]Autopilot, error) {
rows, err := q.db.Query(ctx, pauseAutopilotsByUnrunnableSquad, squadID)
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,
&i.AssigneeType,
&i.ProjectID,
&i.PauseReason,
); 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 :one
WITH updated_run AS (
UPDATE autopilot_run AS ar
SET status = 'failed',
completed_at = now(),
failure_reason = 'recovered partial dispatch (crashed before downstream creation)',
reason_code = 'internal_error',
planned_at = NULL
WHERE ar.id = $1
AND (
ar.status = 'pending'
OR (ar.status = 'issue_created' AND ar.issue_id IS NULL)
OR (ar.status = 'running' AND ar.task_id IS NULL)
)
AND NOT EXISTS (
SELECT 1
FROM agent_task_queue task
WHERE task.autopilot_run_id = ar.id
)
RETURNING ar.quota_reservation_id
), locked_reservation AS MATERIALIZED (
SELECT qr.id, qr.workspace_id, qr.period_start, qr.period_end, qr.policy_revision, qr.subscription_version, qr.source, qr.idempotency_key, qr.state, qr.created_at, qr.finalized_at
FROM autopilot_quota_reservation qr
JOIN updated_run ar ON ar.quota_reservation_id = qr.id
WHERE qr.state = 'reserved'
FOR UPDATE
), released_reservation AS (
UPDATE autopilot_quota_reservation AS qr
SET state = 'released', finalized_at = now()
FROM locked_reservation AS locked
WHERE qr.id = locked.id
AND EXISTS (
SELECT 1 FROM autopilot_quota_period p
WHERE p.workspace_id = locked.workspace_id
AND p.period_start = locked.period_start
AND p.period_end = locked.period_end
)
RETURNING locked.workspace_id, locked.period_start, locked.period_end
), settled_period AS (
UPDATE autopilot_quota_period AS p
SET reserved_count = reserved_count - 1,
updated_at = now()
FROM released_reservation AS released
WHERE p.workspace_id = released.workspace_id
AND p.period_start = released.period_start
AND p.period_end = released.period_end
RETURNING p.workspace_id
)
SELECT count(*)::bigint FROM updated_run
`
// 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) (int64, error) {
row := q.db.QueryRow(ctx, recoverPartialAutopilotRun, id)
var column_1 int64
err := row.Scan(&column_1)
return column_1, 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, created_by_type, created_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,
&i.CreatedByType,
&i.CreatedByID,
)
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), recording who is now
// responsible for its config (MUL-4302).
//
// Since MUL-6951 this changes NOTHING about the runs it fires: they act as, and
// are accountable to, the trigger's immutable created_by. An edit must not be able
// to re-authorize the automation as the editor (Bohan's ruling), so this statement
// deliberately does not touch created_by, and published_by is now a config-audit
// column only.
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, created_by_type, created_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,
&i.CreatedByType,
&i.CreatedByID,
)
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, created_by_type, created_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,
&i.CreatedByType,
&i.CreatedByID,
)
return i, err
}
const systemPauseAutopilot = `-- name: SystemPauseAutopilot :one
UPDATE autopilot
SET status = 'paused', pause_reason = NULL, 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, pause_reason
`
// 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,
&i.PauseReason,
)
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),
pause_reason = CASE
WHEN $6::text IS NOT NULL THEN NULL
ELSE pause_reason
END,
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, pause_reason
`
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,
&i.PauseReason,
)
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, quota_reservation_id, reason_code
`
type UpdateAutopilotRunCompletedParams struct {
ID pgtype.UUID `json:"id"`
Result []byte `json:"result"`
}
// Quota safety: only use for a run known to have no reservation. Normal
// terminal paths must call UpdateAutopilotRunTerminalWithQuota instead.
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,
&i.QuotaReservationID,
&i.ReasonCode,
)
return i, err
}
const updateAutopilotRunFailed = `-- name: UpdateAutopilotRunFailed :one
UPDATE autopilot_run
SET status = 'failed', completed_at = now(), failure_reason = $2,
reason_code = $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, quota_reservation_id, reason_code
`
type UpdateAutopilotRunFailedParams struct {
ID pgtype.UUID `json:"id"`
FailureReason pgtype.Text `json:"failure_reason"`
ReasonCode pgtype.Text `json:"reason_code"`
}
// Quota safety: only use for a run known to have no reservation. Normal
// terminal paths must call UpdateAutopilotRunTerminalWithQuota instead.
func (q *Queries) UpdateAutopilotRunFailed(ctx context.Context, arg UpdateAutopilotRunFailedParams) (AutopilotRun, error) {
row := q.db.QueryRow(ctx, updateAutopilotRunFailed, arg.ID, arg.FailureReason, arg.ReasonCode)
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,
&i.QuotaReservationID,
&i.ReasonCode,
)
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, quota_reservation_id, reason_code
`
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,
&i.QuotaReservationID,
&i.ReasonCode,
)
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, quota_reservation_id, reason_code
`
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,
&i.QuotaReservationID,
&i.ReasonCode,
)
return i, err
}
const updateAutopilotRunSkipped = `-- name: UpdateAutopilotRunSkipped :one
UPDATE autopilot_run
SET status = 'skipped', completed_at = now(), failure_reason = $2,
reason_code = $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, quota_reservation_id, reason_code
`
type UpdateAutopilotRunSkippedParams struct {
ID pgtype.UUID `json:"id"`
FailureReason pgtype.Text `json:"failure_reason"`
ReasonCode pgtype.Text `json:"reason_code"`
}
// Quota safety: this is only for pre-admission skipped rows, which never have
// reservations. Post-admission skips use UpdateAutopilotRunTerminalWithQuota.
// 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, arg.ReasonCode)
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,
&i.QuotaReservationID,
&i.ReasonCode,
)
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, quota_reservation_id, reason_code
`
type UpdateAutopilotRunSkippedWithResultParams struct {
ID pgtype.UUID `json:"id"`
FailureReason pgtype.Text `json:"failure_reason"`
Result []byte `json:"result"`
}
// Quota safety: legacy no-reservation helper. New terminal paths must call
// UpdateAutopilotRunTerminalWithQuota instead.
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,
&i.QuotaReservationID,
&i.ReasonCode,
)
return i, err
}
const updateAutopilotRunTerminalWithQuota = `-- name: UpdateAutopilotRunTerminalWithQuota :one
WITH updated_run AS (
UPDATE autopilot_run AS ar
SET status = $1::text,
completed_at = now(),
result = CASE
WHEN $1::text = 'completed' THEN $2::jsonb
ELSE ar.result
END,
failure_reason = CASE
WHEN $1::text IN ('failed', 'skipped') THEN $3::text
ELSE ar.failure_reason
END,
reason_code = CASE
WHEN $1::text IN ('failed', 'skipped') THEN $4::text
ELSE ar.reason_code
END
WHERE ar.id = $5
RETURNING ar.id, ar.autopilot_id, ar.trigger_id, ar.source, ar.status, ar.issue_id, ar.task_id, ar.triggered_at, ar.completed_at, ar.failure_reason, ar.trigger_payload, ar.result, ar.created_at, ar.squad_id, ar.planned_at, ar.webhook_delivery_id, ar.quota_reservation_id, ar.reason_code
), locked_reservation AS MATERIALIZED (
SELECT qr.id, qr.workspace_id, qr.period_start, qr.period_end, qr.policy_revision, qr.subscription_version, qr.source, qr.idempotency_key, qr.state, qr.created_at, qr.finalized_at
FROM autopilot_quota_reservation qr
JOIN updated_run ar ON ar.quota_reservation_id = qr.id
WHERE qr.state = 'reserved'
FOR UPDATE
), finalized_reservation AS (
UPDATE autopilot_quota_reservation AS qr
SET state = CASE WHEN $6::boolean THEN 'consumed' ELSE 'released' END,
finalized_at = now()
FROM locked_reservation AS locked
WHERE qr.id = locked.id
AND EXISTS (
SELECT 1 FROM autopilot_quota_period p
WHERE p.workspace_id = locked.workspace_id
AND p.period_start = locked.period_start
AND p.period_end = locked.period_end
)
RETURNING locked.workspace_id, locked.period_start, locked.period_end
), settled_period AS (
UPDATE autopilot_quota_period AS p
SET reserved_count = reserved_count - 1,
used_count = used_count + CASE WHEN $6::boolean THEN 1 ELSE 0 END,
updated_at = now()
FROM finalized_reservation AS finalized
WHERE p.workspace_id = finalized.workspace_id
AND p.period_start = finalized.period_start
AND p.period_end = finalized.period_end
RETURNING p.workspace_id
)
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, quota_reservation_id, reason_code FROM updated_run
`
type UpdateAutopilotRunTerminalWithQuotaParams struct {
TerminalStatus string `json:"terminal_status"`
Result []byte `json:"result"`
FailureReason pgtype.Text `json:"failure_reason"`
ReasonCode pgtype.Text `json:"reason_code"`
RunID pgtype.UUID `json:"run_id"`
Consume bool `json:"consume"`
}
type UpdateAutopilotRunTerminalWithQuotaRow struct {
ID pgtype.UUID `json:"id"`
AutopilotID pgtype.UUID `json:"autopilot_id"`
TriggerID pgtype.UUID `json:"trigger_id"`
Source string `json:"source"`
Status string `json:"status"`
IssueID pgtype.UUID `json:"issue_id"`
TaskID pgtype.UUID `json:"task_id"`
TriggeredAt pgtype.Timestamptz `json:"triggered_at"`
CompletedAt pgtype.Timestamptz `json:"completed_at"`
FailureReason pgtype.Text `json:"failure_reason"`
TriggerPayload []byte `json:"trigger_payload"`
Result []byte `json:"result"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
SquadID pgtype.UUID `json:"squad_id"`
PlannedAt pgtype.Timestamptz `json:"planned_at"`
WebhookDeliveryID pgtype.UUID `json:"webhook_delivery_id"`
QuotaReservationID pgtype.UUID `json:"quota_reservation_id"`
ReasonCode pgtype.Text `json:"reason_code"`
}
// Finalizes a run and its still-reserved quota slot in one statement. Runs
// created while the entitlement gate is off have a NULL reservation ID, so
// the quota CTEs become empty without an extra BEGIN/COMMIT round trip.
// Consumed slots are deliberately immutable: create_issue is chargeable once
// the issue exists, even if that issue is later blocked, cancelled, or deleted.
// The CTE sequence is: update the run, lock a still-reserved slot, finalize
// that slot exactly once, then move one unit from reserved_count to either
// used_count (consume) or nowhere (release). used_count never decreases.
func (q *Queries) UpdateAutopilotRunTerminalWithQuota(ctx context.Context, arg UpdateAutopilotRunTerminalWithQuotaParams) (UpdateAutopilotRunTerminalWithQuotaRow, error) {
row := q.db.QueryRow(ctx, updateAutopilotRunTerminalWithQuota,
arg.TerminalStatus,
arg.Result,
arg.FailureReason,
arg.ReasonCode,
arg.RunID,
arg.Consume,
)
var i UpdateAutopilotRunTerminalWithQuotaRow
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,
&i.QuotaReservationID,
&i.ReasonCode,
)
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, created_by_type, created_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,
&i.CreatedByType,
&i.CreatedByID,
)
return i, err
}