mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
* feat(workflows): workspace workflows with step handoffs (MUL-7420)
A workflow is a named, workspace-level selection of statuses from the
shared status catalog, in board order, with an optional handoff on each
step. Projects opt in through project.workflow_id; a project without one
keeps the whole catalog (the implicit Default workflow), so existing
workspaces see no change.
- issue_workflow table + project.workflow_id, CRUD API under
/api/issue-workflows, POST /api/projects/{id}/workflow with a dry-run
status mapping plan. Only issues on statuses the target workflow does
not list are remapped, and remapping never hands issues off.
- Issue writes keep the status inside the project's workflow. Entering a
step with a handler through a status change assigns the issue to that
handler and starts its run with the step's brief as the handoff note;
an explicit assignee in the same write wins. Creating an issue starts
at the workflow's starting step.
- Task claims carry the issue's project workflow, and the brief renders
a Project Workflow section so agents move along this project's
statuses instead of the built-in ones.
- Statuses a workflow lists cannot be archived.
- Adoption is gated by the project_workflows_v1 flag; existing
workflows keep working and projects can always switch back to Default.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(workflows): workflow settings, project picker and workflow-aware boards (MUL-7420)
- Settings → Workflows (behind project_workflows_v1): the Default
workflow plus named workflows, each shown as its step chain with
handlers and the projects using it; an editor to pick statuses from the
library, order them, mark the starting step, and set each step's
handoff, instructions and next/back statuses.
- Project Workflow property: switching dry-runs first and asks where
issues on unlisted statuses go; nothing else moves and nothing is
handed off.
- A project board's columns are its workflow's steps, and each column
head names who entering it hands the issue to.
- Status pickers offer only the issue's workflow steps and say who each
hands off to; boards refuse drops onto statuses the dragged card's
workflow does not list.
- New issues in a workflow project start on its starting step, with a
hint naming who they will be handed to.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(workflows): name built-in steps once in the brief; roomier picker hints (MUL-7420)
Built-in statuses have no stored name, so the handoff brief printed
`todo` todo. Print the key alone when the name adds nothing.
The status picker widens for workflow projects and shows the handoff as
an arrow and name, so step names no longer truncate to one letter.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(workflows): handoff previews and a single timeline entry per handoff (MUL-7420)
- GET /api/issues/{id}/workflow-handoff?status= says what moving an issue
would do: who it goes to, which runs of its current agent are still
active, and the brief the handler receives. Backs the confirmation.
- POST /api/issue-workflows/preview-brief renders a step's brief from an
unsaved workflow, for the editor's preview.
- Mapping plans list the statuses that stay, so the switch dialog can say
which issues are untouched.
- A handoff is recorded on the status_changed activity (workflow, handler,
whether a run started, the brief) instead of a separate assignee row.
- The brief now reads like the one shown in the design: the workflow, the
current step, its instructions, the statuses, and the commands to hand
off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(workflows): rebuild the workflow UI to the reviewed design (MUL-7420)
The first cut used generic controls where the design had purpose-built
ones. This brings every surface up to the mockup:
- Settings → Workflows is one page: the workflows (step chain with
handler avatars, handoff count, projects using it; Default first) and,
below, the shared status library with each status's workflow usage.
- The workflow editor is a full page instead of a dialog: draggable step
cards summarising each handoff, and a panel for the selected step —
swap its status, pick the handler with a segmented control and a
searchable target, handoff instructions, next/back statuses, a preview
of the brief the handler receives, duplicate and delete.
- The project Workflow picker lists each workflow's statuses, handoffs and
projects, links to settings, and always confirms the switch with what
moves and what stays.
- Moving an issue into a handoff step (status picker, table, board drop)
asks first: who it goes from and to, a default-on option to stop the
previous agent's run, and the brief.
- Status pickers name the workflow and project, show each step's handler
with an avatar or that the assignee stays, and explain themselves.
- Boards: the handler chip in project column heads, hatched columns a
dragged card cannot enter with the reason, and a drop hint naming who
the card will go to.
- The timeline records a handoff under its status change, with the brief
the run received; the issue sidebar shows the workflow it follows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(workflows): word a queued previous run as queued in the handoff confirm
The confirm dialog said the previous agent was "still running (for N
minutes)" even when its task had only been queued. Queued runs now read
as queued, and the checkbox cancels them instead of stopping them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(workflows): refuse a run's status change once the issue left its step
A run decides where the issue goes next from the step it was queued for.
When someone else moved the issue while the run worked, applying that
decision undid their move: in the live Grok run, a PM parked NW-1 as
blocked and the still-running agent moved it straight back to code
review.
Tasks now record the issue's status when they are queued
(agent_task_queue.workflow_step, written by the task INSERTs; automatic
retries keep their parent's step), and a run's own status changes move
it along. In a workflow project, an agent status change from a run whose
step no longer matches the issue is refused with 409
workflow_step_moved, naming where the issue is now and who moved it, so
the agent stops instead of retrying.
A handoff away from a squad now also stops the runs made on the squad's
behalf, and stopped runs record who stopped them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(workflows): keep every status entry point inside the issue's workflow
The issue context menu listed every catalog status and applied handoff
steps without the confirmation the status picker and board show, so a
right-click could pick a status the workflow refuses or start an agent
unasked. It now offers the workflow's steps and opens the handoff
confirmation through a shared modal.
One-click status actions no longer assume built-in statuses a workflow
may not list: "mark done" lands on the workflow's done step (hidden when
it has none), un-marking a duplicate returns to the workflow's start,
and "mark duplicate" is offered only when the workflow lists cancelled.
The batch toolbar offers a single project's workflow statuses when the
selection sits in one project.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(workflows): keep issue wakeups consistent with workflow steps
Three places where project workflows and issue wakeups met:
- A wakeup run is queued when it fires, so it took the issue's status at
that moment as its workflow step. A wakeup an agent registered then
bypassed the stale-step refusal: parked by a PM, the issue could be
moved on by the agent's "check CI, then hand to review" wakeup. It now
keeps the registering run's step; a member's wakeup still starts from
the issue's status.
- Retries carried handoff_note only for wakeup runs, so a retried
workflow handoff lost its brief. Rows with a workflow step keep it.
- Switching or editing a workflow remaps statuses in bulk without the
close rule, so an issue remapped into a done status kept its wakeups
and pending wakeup runs. The remap now ends them like any status write.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(wakeups): don't also wake the agent a write hands the issue to
A workflow handoff or an assignment already starts the receiving agent's
run. If that agent also subscribed an event wakeup to the issue's status
or assignee, the same write queued a second run for one fact, and
workflows made such writes frequent.
The write now names the agent it hands the issue to in the
transaction-local multica.handed_to_agent (the new agent assignee, a
squad's leader, or a step's handler even when it already owned the
issue), and wakeup capture skips that agent's subscriptions for the
write's issue.* events. Other agents' wakeups, and task, comment and
reaction events, are captured as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(workflows): tell agents not to watch workflow issues with wakeups
The handoff brief and the runtime's project workflow section now say
that each step's handler gets a run when the issue enters the step, so
wakeups on a workflow issue are for waiting on time or on something
outside it, not on its status, assignee or runs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(workflows): refresh project usage after project changes
Co-authored-by: multica-agent <[email protected]>
* feat(workflows): MUL-7420 split columns from per-project handoffs
Workflows now only define a board's columns between a fixed head
(backlog, todo) and tail (blocked, done, cancelled). Handoffs move to
project.workflow_handoffs, so projects can share columns and still hand
off to different people. Default becomes a real, editable row that every
project without a named workflow follows. Adds project handoff, add-column
(project fork or shared) and project brief preview endpoints; same-agent
adjacent columns re-run the agent.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(core): MUL-7420 project handoffs and Default workflow client model
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(views): MUL-7420 column-header handoffs and project workflow menu
Board column headers set the project's handoff for that column; the
project toolbar gets a workflow menu (full flow, switch, templates) and an
add-column action. Views outside a project show Default's columns, with
other statuses reachable through a status filter. Adds the issue flow card,
create-project workflow choice, a columns-only workflow editor and a
lighter status picker.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(views): MUL-7420 wrap create-project workflow label in a menu group
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(workflows): MUL-7420 keep workflow structure in settings; no squad handoffs
Projects no longer add columns or create workflows: the add-column
endpoint is removed and creating a workflow is admin-only. Handoffs go
to one agent, a member or the project lead; squads route work themselves,
so a handoff to one is rejected, as is the issue-creator handler.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* refactor(core): MUL-7420 drop the project add-column client
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(views): MUL-7420 simpler handoff picker and workflow editor form
The handoff form is one searchable "Hand to" picker (no handoff, the
project lead, an agent or a member); the next status defaults to the next
column and next/back overrides sit under "More options". The workflow
editor gets labelled name and description fields and shows the fixed
statuses as one start and one end row. Templates move from the project
menu to Settings → Workflows → New, and the project board loses its
add-column action.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* refactor(server): MUL-7420 keep handoffs on the workflow, make backlog and blocked optional
Handoffs move back onto workflow steps: a workflow is the whole pipeline,
and a project only picks one. The project lead stays a relative handler, so
projects can still share a workflow. Handoff edits are admin-only, through
the workflow write or PUT /api/issue-workflows/{id}/handoffs/{status}; the
brief preview moves to the workflow too. Drops the unreleased
project.workflow_handoffs migration.
Todo, done and cancelled are required; backlog and blocked are optional and
keep their fixed places. Without blocked, the brief points agents at the
step's back status.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Cursor <[email protected]>
* feat(views): MUL-7420 edit handoffs on the workflow, optional backlog and blocked
The workflow editor sets each column's handoff in its side panel, and
Backlog and Blocked switch on and off. The board's column popover and the
flow panel edit the workflow's handoff, say how many projects it reaches,
and are read-only for non-admins. Drops the per-project handoff client
code.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(wakeups): MUL-7420 keep platform rules on writes that hand off nobody
Migration 579 skips an issue.* event for the agent a write hands the issue
to. Its filter compared agent_id with the transaction's handed_to_agent
setting using IS DISTINCT FROM, which is false when both are NULL. Main's
platform rules (child_done) have no agent_id, so every issue.* event that
handed the issue to nobody was dropped for them: a parent leaving backlog
never woke its agent for sub-issues that closed meanwhile
(TestChildDoneHeldWhileParentInBacklog).
Rules without an agent are never the handoff target, so they always
capture. Extends TestHandoffDoesNotAlsoWakeTheHandler with a platform rule
on a handoff and on a plain status change; the latter fails on the old
filter.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(views): MUL-7420 three-way handoff choice; quieter move confirm
The handoff form picks one of three directions, as in the mockup: no
handoff, hand to an agent (starts a run), or hand to a member, project lead
included (assignee only). What the step does, where it goes next and the
back status only reach an agent's brief, so they show and save for agent
handoffs alone.
The board's move confirmation keeps the agent's brief collapsed. Create
project shows the workflow picker even when Default is the only workflow,
with a note on where to make more. The hidden-columns panel says a board's
columns come from its workflow, with a link to manage workflows for admins.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(workflows): confirm handoff on project move, per-project handoff copy, two-pane editor (MUL-7420)
- A single issue moved into a workflow project from issue detail confirms
the handoff of the column it lands on; the write opts in with
enter_workflow_step. Batch, API and CLI moves stay administrative.
- The handoff preview takes project_id to preview such a move.
- Saving a shared workflow's handoff from a board can keep the change to
that project: the server copies the workflow for it and switches the
project in one transaction (copy_for_project_id).
- The column popover says which workflow it edits and who else uses it.
- The workflow editor is a step list with a detail panel for the
selected step.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* style: gofmt workflow tests
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(views): MUL-7420 show next/back always; edit workflow name and description in place
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(views): MUL-7420 trim copy on the workflows settings page
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix: MUL-7420 keep pre-workflow behavior for workspaces that never adopt workflows
The run step guard now applies only to workflows that hand off, and the
standalone Statuses settings page keeps its original header.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(views): MUL-7420 use a solid placeholder tone on the workflow header
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(workflows): keep the runtime brief cache-stable; fix project board and batch status gaps (MUL-7420)
- Move the project workflow out of the runtime brief into the per-turn
prompt so CLAUDE.md stays byte-identical across steps and issues.
- An explicit status filter overrides the project workflow's columns; the
hidden-columns panel lists statuses outside the workflow.
- Batch status offers only statuses every selected workflow allows; a
partial batch rolls back, refetches and warns.
- setProjectWorkflow returns void instead of an unparsed body.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Lambda <[email protected]>
Co-authored-by: Cursor <[email protected]>
832 lines
28 KiB
Go
832 lines
28 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: supplement.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const ackTaskSupplementDelivered = `-- name: AckTaskSupplementDelivered :one
|
|
WITH task AS MATERIALIZED (
|
|
SELECT id FROM agent_task_queue WHERE id = $2 FOR UPDATE
|
|
)
|
|
UPDATE task_supplement s
|
|
SET status = 'delivered',
|
|
delivered_at = COALESCE(delivered_at, now()),
|
|
failure_reason = NULL,
|
|
updated_at = now()
|
|
FROM task t
|
|
WHERE s.task_id = t.id
|
|
AND s.comment_id = $1
|
|
AND (s.status IN ('delivering', 'delivered')
|
|
OR (s.status = 'failed' AND s.failure_reason = 'turn_ended' AND s.attempt_count > 0))
|
|
RETURNING s.task_id, s.workspace_id, s.issue_id, s.comment_id, s.author_id, s.client_request_id, s.status, s.failure_reason, s.attempt_count, s.created_at, s.updated_at, s.delivered_at
|
|
`
|
|
|
|
type AckTaskSupplementDeliveredParams struct {
|
|
CommentID pgtype.UUID `json:"comment_id"`
|
|
TaskID pgtype.UUID `json:"task_id"`
|
|
}
|
|
|
|
// The provider can accept input before completion while its HTTP acknowledgement
|
|
// arrives afterward. Serialize with settlement and preserve that successful write.
|
|
func (q *Queries) AckTaskSupplementDelivered(ctx context.Context, arg AckTaskSupplementDeliveredParams) (TaskSupplement, error) {
|
|
row := q.db.QueryRow(ctx, ackTaskSupplementDelivered, arg.CommentID, arg.TaskID)
|
|
var i TaskSupplement
|
|
err := row.Scan(
|
|
&i.TaskID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.CommentID,
|
|
&i.AuthorID,
|
|
&i.ClientRequestID,
|
|
&i.Status,
|
|
&i.FailureReason,
|
|
&i.AttemptCount,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.DeliveredAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const ackTaskSupplementFailed = `-- name: AckTaskSupplementFailed :one
|
|
UPDATE task_supplement
|
|
SET status = 'failed',
|
|
failure_reason = $1,
|
|
updated_at = now()
|
|
WHERE task_id = $2
|
|
AND comment_id = $3
|
|
AND status = 'delivering'
|
|
RETURNING task_id, workspace_id, issue_id, comment_id, author_id, client_request_id, status, failure_reason, attempt_count, created_at, updated_at, delivered_at
|
|
`
|
|
|
|
type AckTaskSupplementFailedParams struct {
|
|
FailureReason pgtype.Text `json:"failure_reason"`
|
|
TaskID pgtype.UUID `json:"task_id"`
|
|
CommentID pgtype.UUID `json:"comment_id"`
|
|
}
|
|
|
|
func (q *Queries) AckTaskSupplementFailed(ctx context.Context, arg AckTaskSupplementFailedParams) (TaskSupplement, error) {
|
|
row := q.db.QueryRow(ctx, ackTaskSupplementFailed, arg.FailureReason, arg.TaskID, arg.CommentID)
|
|
var i TaskSupplement
|
|
err := row.Scan(
|
|
&i.TaskID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.CommentID,
|
|
&i.AuthorID,
|
|
&i.ClientRequestID,
|
|
&i.Status,
|
|
&i.FailureReason,
|
|
&i.AttemptCount,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.DeliveredAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const bindCommentTaskSupplement = `-- name: BindCommentTaskSupplement :one
|
|
WITH locked_task AS MATERIALIZED (
|
|
SELECT t.id, t.issue_id, t.runtime_id
|
|
FROM agent_task_queue t
|
|
JOIN agent_runtime r ON r.id = t.runtime_id
|
|
JOIN task_supplement_capability cap ON cap.task_id = t.id
|
|
WHERE t.id = $1
|
|
AND t.issue_id = $2
|
|
AND t.agent_id = $3
|
|
AND r.workspace_id = $4
|
|
AND t.status = 'running'
|
|
AND cap.capability = 'task-supplement-v1'
|
|
AND r.provider IN ('codex', 'claude')
|
|
FOR UPDATE OF t
|
|
), inserted AS (
|
|
INSERT INTO task_supplement (
|
|
task_id, workspace_id, issue_id, comment_id, author_id,
|
|
client_request_id, status
|
|
)
|
|
SELECT t.id, $4, t.issue_id, $5, $6,
|
|
$7, 'pending'
|
|
FROM locked_task t
|
|
RETURNING task_id, workspace_id, issue_id, comment_id, author_id, client_request_id, status, failure_reason, attempt_count, created_at, updated_at, delivered_at
|
|
)
|
|
SELECT inserted.task_id, inserted.workspace_id, inserted.issue_id, inserted.comment_id, inserted.author_id, inserted.client_request_id, inserted.status, inserted.failure_reason, inserted.attempt_count, inserted.created_at, inserted.updated_at, inserted.delivered_at, locked_task.runtime_id
|
|
FROM inserted
|
|
JOIN locked_task ON locked_task.id = inserted.task_id
|
|
`
|
|
|
|
type BindCommentTaskSupplementParams struct {
|
|
TaskID pgtype.UUID `json:"task_id"`
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
CommentID pgtype.UUID `json:"comment_id"`
|
|
AuthorID pgtype.UUID `json:"author_id"`
|
|
ClientRequestID pgtype.UUID `json:"client_request_id"`
|
|
}
|
|
|
|
type BindCommentTaskSupplementRow struct {
|
|
TaskID pgtype.UUID `json:"task_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
CommentID pgtype.UUID `json:"comment_id"`
|
|
AuthorID pgtype.UUID `json:"author_id"`
|
|
ClientRequestID pgtype.UUID `json:"client_request_id"`
|
|
Status string `json:"status"`
|
|
FailureReason pgtype.Text `json:"failure_reason"`
|
|
AttemptCount int32 `json:"attempt_count"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
DeliveredAt pgtype.Timestamptz `json:"delivered_at"`
|
|
RuntimeID pgtype.UUID `json:"runtime_id"`
|
|
}
|
|
|
|
// Binds an ordinary member comment, already created through the normal comment
|
|
// path, to the exact running turn its author chose. Locking the task
|
|
// serializes against terminal transitions: when that turn ended first nothing
|
|
// is bound — never a later turn of the same agent — and the caller keeps the
|
|
// comment's normal trigger instead.
|
|
func (q *Queries) BindCommentTaskSupplement(ctx context.Context, arg BindCommentTaskSupplementParams) (BindCommentTaskSupplementRow, error) {
|
|
row := q.db.QueryRow(ctx, bindCommentTaskSupplement,
|
|
arg.TaskID,
|
|
arg.IssueID,
|
|
arg.AgentID,
|
|
arg.WorkspaceID,
|
|
arg.CommentID,
|
|
arg.AuthorID,
|
|
arg.ClientRequestID,
|
|
)
|
|
var i BindCommentTaskSupplementRow
|
|
err := row.Scan(
|
|
&i.TaskID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.CommentID,
|
|
&i.AuthorID,
|
|
&i.ClientRequestID,
|
|
&i.Status,
|
|
&i.FailureReason,
|
|
&i.AttemptCount,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.DeliveredAt,
|
|
&i.RuntimeID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const claimNextTaskSupplement = `-- name: ClaimNextTaskSupplement :one
|
|
WITH next AS MATERIALIZED (
|
|
SELECT s.comment_id, s.task_id
|
|
FROM task_supplement s
|
|
JOIN agent_task_queue t ON t.id = s.task_id
|
|
JOIN task_supplement_capability cap ON cap.task_id = t.id
|
|
WHERE s.task_id = $1
|
|
AND s.status = 'pending'
|
|
AND t.status = 'running'
|
|
AND cap.capability = 'task-supplement-v1'
|
|
ORDER BY s.created_at, s.comment_id
|
|
FOR UPDATE OF s SKIP LOCKED
|
|
LIMIT 1
|
|
), claimed AS (
|
|
UPDATE task_supplement s
|
|
SET status = 'delivering',
|
|
attempt_count = attempt_count + 1,
|
|
failure_reason = NULL,
|
|
updated_at = now()
|
|
FROM next
|
|
WHERE s.comment_id = next.comment_id
|
|
AND s.task_id = next.task_id
|
|
RETURNING s.task_id, s.workspace_id, s.issue_id, s.comment_id, s.author_id, s.client_request_id, s.status, s.failure_reason, s.attempt_count, s.created_at, s.updated_at, s.delivered_at
|
|
)
|
|
SELECT claimed.comment_id, claimed.attempt_count, c.content,
|
|
COALESCE(NULLIF(btrim(u.name), ''), 'a user')::text AS author_name
|
|
FROM claimed
|
|
JOIN comment c ON c.id = claimed.comment_id
|
|
LEFT JOIN "user" u ON u.id = claimed.author_id
|
|
`
|
|
|
|
type ClaimNextTaskSupplementRow struct {
|
|
CommentID pgtype.UUID `json:"comment_id"`
|
|
AttemptCount int32 `json:"attempt_count"`
|
|
Content string `json:"content"`
|
|
AuthorName string `json:"author_name"`
|
|
}
|
|
|
|
func (q *Queries) ClaimNextTaskSupplement(ctx context.Context, taskID pgtype.UUID) (ClaimNextTaskSupplementRow, error) {
|
|
row := q.db.QueryRow(ctx, claimNextTaskSupplement, taskID)
|
|
var i ClaimNextTaskSupplementRow
|
|
err := row.Scan(
|
|
&i.CommentID,
|
|
&i.AttemptCount,
|
|
&i.Content,
|
|
&i.AuthorName,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const commentHasTaskSupplement = `-- name: CommentHasTaskSupplement :one
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM task_supplement
|
|
WHERE comment_id = $1 AND workspace_id = $2
|
|
) AS bound
|
|
`
|
|
|
|
type CommentHasTaskSupplementParams struct {
|
|
CommentID pgtype.UUID `json:"comment_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) CommentHasTaskSupplement(ctx context.Context, arg CommentHasTaskSupplementParams) (bool, error) {
|
|
row := q.db.QueryRow(ctx, commentHasTaskSupplement, arg.CommentID, arg.WorkspaceID)
|
|
var bound bool
|
|
err := row.Scan(&bound)
|
|
return bound, err
|
|
}
|
|
|
|
const createTaskSupplement = `-- name: CreateTaskSupplement :one
|
|
WITH locked_task AS MATERIALIZED (
|
|
SELECT t.id, t.issue_id, t.agent_id, t.trigger_comment_id
|
|
FROM agent_task_queue t
|
|
JOIN agent_runtime r ON r.id = t.runtime_id
|
|
JOIN task_supplement_capability cap ON cap.task_id = t.id
|
|
WHERE t.id = $1
|
|
AND t.issue_id = $2
|
|
AND r.workspace_id = $3
|
|
AND t.status = 'running'
|
|
AND cap.capability = 'task-supplement-v1'
|
|
AND r.provider IN ('codex', 'claude', 'grok')
|
|
FOR UPDATE OF t
|
|
), touched_issue AS (
|
|
UPDATE issue i SET
|
|
updated_at = now(),
|
|
revision = revision + 1,
|
|
last_activity_at = GREATEST(COALESCE(last_activity_at, updated_at), now())
|
|
FROM locked_task t
|
|
WHERE i.id = t.issue_id AND i.workspace_id = $3
|
|
RETURNING i.id, i.workspace_id, i.revision
|
|
), inserted_comment AS (
|
|
INSERT INTO comment (
|
|
issue_id, workspace_id, author_type, author_id, content, type, parent_id
|
|
)
|
|
SELECT i.id, i.workspace_id, 'member', $4, $5, 'comment', t.trigger_comment_id
|
|
FROM touched_issue i
|
|
JOIN locked_task t ON t.issue_id = i.id
|
|
RETURNING id, issue_id, author_type, author_id, content, type, created_at, updated_at, parent_id, workspace_id, resolved_at, resolved_by_type, resolved_by_id, source_task_id, quick_action_id, via_plugin_id, revision, recovery_settled_at, deleted_at, suppressed_agent_ids
|
|
), inserted_supplement AS (
|
|
INSERT INTO task_supplement (
|
|
task_id, workspace_id, issue_id, comment_id, author_id,
|
|
client_request_id, status
|
|
)
|
|
SELECT t.id, i.workspace_id, i.id, c.id, $4,
|
|
$6,
|
|
'pending'
|
|
FROM locked_task t
|
|
JOIN touched_issue i ON i.id = t.issue_id
|
|
JOIN inserted_comment c ON c.issue_id = i.id
|
|
RETURNING task_id, workspace_id, issue_id, comment_id, author_id, client_request_id, status, failure_reason, attempt_count, created_at, updated_at, delivered_at
|
|
)
|
|
SELECT c.id, c.issue_id, c.author_type, c.author_id, c.content, c.type, c.created_at, c.updated_at, c.parent_id, c.workspace_id, c.resolved_at, c.resolved_by_type, c.resolved_by_id, c.source_task_id, c.quick_action_id, c.via_plugin_id, c.revision, c.recovery_settled_at, c.deleted_at, c.suppressed_agent_ids, i.revision AS issue_revision,
|
|
s.task_id AS supplement_task_id, s.status AS supplement_status,
|
|
s.failure_reason AS supplement_failure_reason,
|
|
s.delivered_at AS supplement_delivered_at
|
|
FROM inserted_comment c
|
|
JOIN touched_issue i ON i.id = c.issue_id
|
|
JOIN inserted_supplement s ON s.comment_id = c.id
|
|
`
|
|
|
|
type CreateTaskSupplementParams struct {
|
|
TaskID pgtype.UUID `json:"task_id"`
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
AuthorID pgtype.UUID `json:"author_id"`
|
|
Content string `json:"content"`
|
|
ClientRequestID pgtype.UUID `json:"client_request_id"`
|
|
}
|
|
|
|
type CreateTaskSupplementRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
AuthorType string `json:"author_type"`
|
|
AuthorID pgtype.UUID `json:"author_id"`
|
|
Content string `json:"content"`
|
|
Type string `json:"type"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
ParentID pgtype.UUID `json:"parent_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ResolvedAt pgtype.Timestamptz `json:"resolved_at"`
|
|
ResolvedByType pgtype.Text `json:"resolved_by_type"`
|
|
ResolvedByID pgtype.UUID `json:"resolved_by_id"`
|
|
SourceTaskID pgtype.UUID `json:"source_task_id"`
|
|
QuickActionID pgtype.UUID `json:"quick_action_id"`
|
|
ViaPluginID pgtype.UUID `json:"via_plugin_id"`
|
|
Revision int64 `json:"revision"`
|
|
RecoverySettledAt pgtype.Timestamptz `json:"recovery_settled_at"`
|
|
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
|
SuppressedAgentIds []pgtype.UUID `json:"suppressed_agent_ids"`
|
|
IssueRevision int64 `json:"issue_revision"`
|
|
SupplementTaskID pgtype.UUID `json:"supplement_task_id"`
|
|
SupplementStatus string `json:"supplement_status"`
|
|
SupplementFailureReason pgtype.Text `json:"supplement_failure_reason"`
|
|
SupplementDeliveredAt pgtype.Timestamptz `json:"supplement_delivered_at"`
|
|
}
|
|
|
|
// Locking the exact task serializes against terminal transitions.
|
|
// Comment creation, explicit task binding and run coverage then
|
|
// commit as one statement: a terminal-race loser creates nothing.
|
|
func (q *Queries) CreateTaskSupplement(ctx context.Context, arg CreateTaskSupplementParams) (CreateTaskSupplementRow, error) {
|
|
row := q.db.QueryRow(ctx, createTaskSupplement,
|
|
arg.TaskID,
|
|
arg.IssueID,
|
|
arg.WorkspaceID,
|
|
arg.AuthorID,
|
|
arg.Content,
|
|
arg.ClientRequestID,
|
|
)
|
|
var i CreateTaskSupplementRow
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.IssueID,
|
|
&i.AuthorType,
|
|
&i.AuthorID,
|
|
&i.Content,
|
|
&i.Type,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.ParentID,
|
|
&i.WorkspaceID,
|
|
&i.ResolvedAt,
|
|
&i.ResolvedByType,
|
|
&i.ResolvedByID,
|
|
&i.SourceTaskID,
|
|
&i.QuickActionID,
|
|
&i.ViaPluginID,
|
|
&i.Revision,
|
|
&i.RecoverySettledAt,
|
|
&i.DeletedAt,
|
|
&i.SuppressedAgentIds,
|
|
&i.IssueRevision,
|
|
&i.SupplementTaskID,
|
|
&i.SupplementStatus,
|
|
&i.SupplementFailureReason,
|
|
&i.SupplementDeliveredAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteTaskSupplementByComment = `-- name: DeleteTaskSupplementByComment :exec
|
|
DELETE FROM task_supplement
|
|
WHERE comment_id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type DeleteTaskSupplementByCommentParams struct {
|
|
CommentID pgtype.UUID `json:"comment_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) DeleteTaskSupplementByComment(ctx context.Context, arg DeleteTaskSupplementByCommentParams) error {
|
|
_, err := q.db.Exec(ctx, deleteTaskSupplementByComment, arg.CommentID, arg.WorkspaceID)
|
|
return err
|
|
}
|
|
|
|
const getTaskSupplementByRequest = `-- name: GetTaskSupplementByRequest :one
|
|
SELECT s.task_id, s.workspace_id, s.issue_id, s.comment_id, s.author_id, s.client_request_id, s.status, s.failure_reason, s.attempt_count, s.created_at, s.updated_at, s.delivered_at
|
|
FROM task_supplement s
|
|
WHERE s.task_id = $1
|
|
AND s.workspace_id = $2
|
|
AND s.author_id = $3
|
|
AND s.client_request_id = $4
|
|
`
|
|
|
|
type GetTaskSupplementByRequestParams struct {
|
|
TaskID pgtype.UUID `json:"task_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
AuthorID pgtype.UUID `json:"author_id"`
|
|
ClientRequestID pgtype.UUID `json:"client_request_id"`
|
|
}
|
|
|
|
func (q *Queries) GetTaskSupplementByRequest(ctx context.Context, arg GetTaskSupplementByRequestParams) (TaskSupplement, error) {
|
|
row := q.db.QueryRow(ctx, getTaskSupplementByRequest,
|
|
arg.TaskID,
|
|
arg.WorkspaceID,
|
|
arg.AuthorID,
|
|
arg.ClientRequestID,
|
|
)
|
|
var i TaskSupplement
|
|
err := row.Scan(
|
|
&i.TaskID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.CommentID,
|
|
&i.AuthorID,
|
|
&i.ClientRequestID,
|
|
&i.Status,
|
|
&i.FailureReason,
|
|
&i.AttemptCount,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.DeliveredAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getTaskSupplementCapability = `-- name: GetTaskSupplementCapability :one
|
|
SELECT task_id, workspace_id, issue_id, capability, created_at FROM task_supplement_capability WHERE task_id = $1
|
|
`
|
|
|
|
func (q *Queries) GetTaskSupplementCapability(ctx context.Context, taskID pgtype.UUID) (TaskSupplementCapability, error) {
|
|
row := q.db.QueryRow(ctx, getTaskSupplementCapability, taskID)
|
|
var i TaskSupplementCapability
|
|
err := row.Scan(
|
|
&i.TaskID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.Capability,
|
|
&i.CreatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getTaskSupplementForRun = `-- name: GetTaskSupplementForRun :one
|
|
SELECT task_id, workspace_id, issue_id, comment_id, author_id, client_request_id, status, failure_reason, attempt_count, created_at, updated_at, delivered_at FROM task_supplement
|
|
WHERE comment_id = $1 AND task_id = $2 AND workspace_id = $3
|
|
`
|
|
|
|
type GetTaskSupplementForRunParams struct {
|
|
CommentID pgtype.UUID `json:"comment_id"`
|
|
TaskID pgtype.UUID `json:"task_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) GetTaskSupplementForRun(ctx context.Context, arg GetTaskSupplementForRunParams) (TaskSupplement, error) {
|
|
row := q.db.QueryRow(ctx, getTaskSupplementForRun, arg.CommentID, arg.TaskID, arg.WorkspaceID)
|
|
var i TaskSupplement
|
|
err := row.Scan(
|
|
&i.TaskID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.CommentID,
|
|
&i.AuthorID,
|
|
&i.ClientRequestID,
|
|
&i.Status,
|
|
&i.FailureReason,
|
|
&i.AttemptCount,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.DeliveredAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listTaskSupplementMetadata = `-- name: ListTaskSupplementMetadata :many
|
|
SELECT cap.task_id, cap.capability,
|
|
COALESCE(array_agg(s.comment_id ORDER BY s.created_at, s.comment_id)
|
|
FILTER (WHERE s.comment_id IS NOT NULL), '{}'::uuid[])::uuid[] AS comment_ids
|
|
FROM task_supplement_capability cap
|
|
LEFT JOIN task_supplement s ON s.task_id = cap.task_id
|
|
WHERE cap.workspace_id = $1
|
|
AND cap.task_id = ANY($2::uuid[])
|
|
GROUP BY cap.task_id, cap.capability
|
|
`
|
|
|
|
type ListTaskSupplementMetadataParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
TaskIds []pgtype.UUID `json:"task_ids"`
|
|
}
|
|
|
|
type ListTaskSupplementMetadataRow struct {
|
|
TaskID pgtype.UUID `json:"task_id"`
|
|
Capability string `json:"capability"`
|
|
CommentIds []pgtype.UUID `json:"comment_ids"`
|
|
}
|
|
|
|
func (q *Queries) ListTaskSupplementMetadata(ctx context.Context, arg ListTaskSupplementMetadataParams) ([]ListTaskSupplementMetadataRow, error) {
|
|
rows, err := q.db.Query(ctx, listTaskSupplementMetadata, arg.WorkspaceID, arg.TaskIds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListTaskSupplementMetadataRow{}
|
|
for rows.Next() {
|
|
var i ListTaskSupplementMetadataRow
|
|
if err := rows.Scan(&i.TaskID, &i.Capability, &i.CommentIds); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listTaskSupplementsByCommentIDs = `-- name: ListTaskSupplementsByCommentIDs :many
|
|
SELECT s.task_id, s.workspace_id, s.issue_id, s.comment_id, s.author_id, s.client_request_id, s.status, s.failure_reason, s.attempt_count, s.created_at, s.updated_at, s.delivered_at, t.agent_id
|
|
FROM task_supplement s
|
|
LEFT JOIN agent_task_queue t ON t.id = s.task_id
|
|
WHERE s.workspace_id = $1
|
|
AND s.comment_id = ANY($2::uuid[])
|
|
ORDER BY s.created_at, s.task_id
|
|
`
|
|
|
|
type ListTaskSupplementsByCommentIDsParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
CommentIds []pgtype.UUID `json:"comment_ids"`
|
|
}
|
|
|
|
type ListTaskSupplementsByCommentIDsRow struct {
|
|
TaskID pgtype.UUID `json:"task_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
CommentID pgtype.UUID `json:"comment_id"`
|
|
AuthorID pgtype.UUID `json:"author_id"`
|
|
ClientRequestID pgtype.UUID `json:"client_request_id"`
|
|
Status string `json:"status"`
|
|
FailureReason pgtype.Text `json:"failure_reason"`
|
|
AttemptCount int32 `json:"attempt_count"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
DeliveredAt pgtype.Timestamptz `json:"delivered_at"`
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
}
|
|
|
|
// One comment may steer several runs; receipts carry the run's agent so a
|
|
// client can say whose turn received it.
|
|
func (q *Queries) ListTaskSupplementsByCommentIDs(ctx context.Context, arg ListTaskSupplementsByCommentIDsParams) ([]ListTaskSupplementsByCommentIDsRow, error) {
|
|
rows, err := q.db.Query(ctx, listTaskSupplementsByCommentIDs, arg.WorkspaceID, arg.CommentIds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListTaskSupplementsByCommentIDsRow{}
|
|
for rows.Next() {
|
|
var i ListTaskSupplementsByCommentIDsRow
|
|
if err := rows.Scan(
|
|
&i.TaskID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.CommentID,
|
|
&i.AuthorID,
|
|
&i.ClientRequestID,
|
|
&i.Status,
|
|
&i.FailureReason,
|
|
&i.AttemptCount,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.DeliveredAt,
|
|
&i.AgentID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const lockTaskSupplementsByComment = `-- name: LockTaskSupplementsByComment :many
|
|
SELECT task_id, workspace_id, issue_id, comment_id, author_id, client_request_id, status, failure_reason, attempt_count, created_at, updated_at, delivered_at FROM task_supplement
|
|
WHERE comment_id = $1 AND workspace_id = $2
|
|
FOR UPDATE
|
|
`
|
|
|
|
type LockTaskSupplementsByCommentParams struct {
|
|
CommentID pgtype.UUID `json:"comment_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) LockTaskSupplementsByComment(ctx context.Context, arg LockTaskSupplementsByCommentParams) ([]TaskSupplement, error) {
|
|
rows, err := q.db.Query(ctx, lockTaskSupplementsByComment, arg.CommentID, arg.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []TaskSupplement{}
|
|
for rows.Next() {
|
|
var i TaskSupplement
|
|
if err := rows.Scan(
|
|
&i.TaskID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.CommentID,
|
|
&i.AuthorID,
|
|
&i.ClientRequestID,
|
|
&i.Status,
|
|
&i.FailureReason,
|
|
&i.AttemptCount,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.DeliveredAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const retryTaskSupplement = `-- name: RetryTaskSupplement :one
|
|
WITH comment AS MATERIALIZED (
|
|
SELECT id FROM comment
|
|
WHERE id = $1 AND workspace_id = $2 AND deleted_at IS NULL
|
|
FOR UPDATE
|
|
), active_task AS MATERIALIZED (
|
|
SELECT agent_task_queue.id FROM agent_task_queue
|
|
WHERE agent_task_queue.id = $3
|
|
AND agent_task_queue.issue_id = $4
|
|
AND agent_task_queue.status = 'running'
|
|
AND EXISTS (SELECT 1 FROM comment)
|
|
AND EXISTS (
|
|
SELECT 1 FROM task_supplement_capability cap
|
|
WHERE cap.task_id = agent_task_queue.id
|
|
AND cap.capability = 'task-supplement-v1'
|
|
)
|
|
FOR UPDATE OF agent_task_queue
|
|
)
|
|
UPDATE task_supplement s
|
|
SET status = 'pending',
|
|
failure_reason = NULL,
|
|
updated_at = now()
|
|
FROM active_task t
|
|
WHERE s.task_id = t.id
|
|
AND s.comment_id = $1
|
|
AND s.workspace_id = $2
|
|
AND s.status = 'failed'
|
|
RETURNING s.task_id, s.workspace_id, s.issue_id, s.comment_id, s.author_id, s.client_request_id, s.status, s.failure_reason, s.attempt_count, s.created_at, s.updated_at, s.delivered_at
|
|
`
|
|
|
|
type RetryTaskSupplementParams struct {
|
|
CommentID pgtype.UUID `json:"comment_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
TaskID pgtype.UUID `json:"task_id"`
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
}
|
|
|
|
func (q *Queries) RetryTaskSupplement(ctx context.Context, arg RetryTaskSupplementParams) (TaskSupplement, error) {
|
|
row := q.db.QueryRow(ctx, retryTaskSupplement,
|
|
arg.CommentID,
|
|
arg.WorkspaceID,
|
|
arg.TaskID,
|
|
arg.IssueID,
|
|
)
|
|
var i TaskSupplement
|
|
err := row.Scan(
|
|
&i.TaskID,
|
|
&i.WorkspaceID,
|
|
&i.IssueID,
|
|
&i.CommentID,
|
|
&i.AuthorID,
|
|
&i.ClientRequestID,
|
|
&i.Status,
|
|
&i.FailureReason,
|
|
&i.AttemptCount,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.DeliveredAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const settleTerminalTaskSupplements = `-- name: SettleTerminalTaskSupplements :execrows
|
|
UPDATE task_supplement AS supplement
|
|
SET status = 'failed',
|
|
failure_reason = 'turn_ended',
|
|
updated_at = now()
|
|
FROM agent_task_queue AS task
|
|
WHERE supplement.task_id = task.id
|
|
AND task.id = ANY($1::uuid[])
|
|
AND task.status IN ('completed', 'failed', 'cancelled')
|
|
AND supplement.status IN ('pending', 'delivering')
|
|
`
|
|
|
|
// Application terminal transitions call this in the same transaction as the
|
|
// agent_task_queue update. Re-checking the persisted task status makes an
|
|
// accidental early call a no-op while preserving the task-row -> supplement
|
|
// lock order used by creation and delivery acknowledgement.
|
|
func (q *Queries) SettleTerminalTaskSupplements(ctx context.Context, taskIds []pgtype.UUID) (int64, error) {
|
|
result, err := q.db.Exec(ctx, settleTerminalTaskSupplements, taskIds)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const startAgentTaskWithSupplement = `-- name: StartAgentTaskWithSupplement :one
|
|
WITH candidate AS MATERIALIZED (
|
|
SELECT t.id, t.issue_id, r.workspace_id, r.provider
|
|
FROM agent_task_queue t
|
|
JOIN agent_runtime r ON r.id = t.runtime_id
|
|
WHERE t.id = $1
|
|
AND t.status IN ('dispatched', 'waiting_local_directory')
|
|
FOR UPDATE OF t
|
|
), capability AS (
|
|
INSERT INTO task_supplement_capability (task_id, workspace_id, issue_id, capability)
|
|
SELECT id, workspace_id, issue_id, 'task-supplement-v1'
|
|
FROM candidate
|
|
WHERE $2::boolean
|
|
AND provider IN ('codex', 'claude', 'grok')
|
|
AND issue_id IS NOT NULL
|
|
ON CONFLICT DO NOTHING
|
|
RETURNING task_id
|
|
)
|
|
UPDATE agent_task_queue t
|
|
SET status = 'running',
|
|
started_at = now(),
|
|
wait_reason = NULL,
|
|
prepare_lease_expires_at = NULL
|
|
FROM candidate
|
|
WHERE t.id = candidate.id
|
|
-- Reference the data-modifying CTE explicitly: capability persistence and
|
|
-- the returned running row are one indivisible statement.
|
|
AND (SELECT count(*) FROM capability) >= 0
|
|
RETURNING t.id, t.agent_id, t.issue_id, t.status, t.priority, t.dispatched_at, t.started_at, t.completed_at, t.result, t.error, t.created_at, t.context, t.runtime_id, t.session_id, t.work_dir, t.trigger_comment_id, t.chat_session_id, t.autopilot_run_id, t.attempt, t.max_attempts, t.parent_task_id, t.failure_reason, t.trigger_summary, t.force_fresh_session, t.is_leader_task, t.wait_reason, t.initiator_user_id, t.handoff_note, t.prepare_lease_expires_at, t.squad_id, t.runtime_mcp_overlay, t.escalation_for_task_id, t.fire_at, t.originator_user_id, t.runtime_connected_apps, t.coalesced_comment_ids, t.delivered_comment_ids, t.chat_input_task_id, t.chat_finalize_deferred_at, t.originator_source, t.delegated_from_task_id, t.retry_of_task_id, t.rerun_of_task_id, t.rule_version_id, t.trigger_evidence_kind, t.trigger_evidence_ref_id, t.accountable_user_id, t.session_rollout_missing, t.retired_session_id, t.quick_actions_disabled, t.regenerate_quick_actions_for, t.branch_name, t.durable_work_dir, t.channel_context_revision, t.comment_thread_id, t.cancelled_by_type, t.cancelled_by_id, t.cancelled_by_name, t.issue_snapshot, t.workflow_step
|
|
`
|
|
|
|
type StartAgentTaskWithSupplementParams struct {
|
|
TaskID pgtype.UUID `json:"task_id"`
|
|
EnableTaskSupplement bool `json:"enable_task_supplement"`
|
|
}
|
|
|
|
// Starting the task and recording the exact daemon/server capability handshake
|
|
// are one state transition. A missing row is the fail-closed value for old
|
|
// daemons, old servers, unsupported providers and application rollback.
|
|
func (q *Queries) StartAgentTaskWithSupplement(ctx context.Context, arg StartAgentTaskWithSupplementParams) (AgentTaskQueue, error) {
|
|
row := q.db.QueryRow(ctx, startAgentTaskWithSupplement, arg.TaskID, arg.EnableTaskSupplement)
|
|
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,
|
|
&i.WorkflowStep,
|
|
)
|
|
return i, err
|
|
}
|