mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
* fix(agent): join Pi cancellation cleanup and guard continuation Co-authored-by: multica-agent <[email protected]> * fix(agent): preserve Pi completion and negotiate cleanup Co-authored-by: multica-agent <[email protected]> * perf(agent): avoid Pi exit delay and index cleanup guards Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Multica Eve <[email protected]> Co-authored-by: multica-agent <[email protected]>
274 lines
10 KiB
SQL
274 lines
10 KiB
SQL
-- name: StartAgentTaskWithSupplement :one
|
|
-- 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.
|
|
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 = @task_id
|
|
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 @enable_task_supplement::boolean
|
|
AND provider IN ('codex', 'claude', 'grok', 'cursor', 'kiro')
|
|
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,
|
|
result = CASE WHEN @enable_process_cleanup::boolean AND candidate.provider IN ('pi', 'omp')
|
|
THEN COALESCE(t.result, '{}'::jsonb) || '{"process_cleanup_capability":"pi-process-cleanup-v1"}'::jsonb
|
|
ELSE t.result END
|
|
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.*;
|
|
|
|
-- name: CreateTaskSupplement :one
|
|
-- 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.
|
|
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 = @task_id
|
|
AND t.issue_id = @issue_id
|
|
AND r.workspace_id = @workspace_id
|
|
AND t.status = 'running'
|
|
AND cap.capability = 'task-supplement-v1'
|
|
AND r.provider IN ('codex', 'claude', 'grok', 'cursor', 'kiro')
|
|
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 = @workspace_id
|
|
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', @author_id, @content, 'comment', t.trigger_comment_id
|
|
FROM touched_issue i
|
|
JOIN locked_task t ON t.issue_id = i.id
|
|
RETURNING *
|
|
), 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, @author_id,
|
|
@client_request_id,
|
|
'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 *
|
|
)
|
|
SELECT c.*, 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;
|
|
|
|
-- name: GetTaskSupplementByRequest :one
|
|
SELECT s.*
|
|
FROM task_supplement s
|
|
WHERE s.task_id = @task_id
|
|
AND s.workspace_id = @workspace_id
|
|
AND s.author_id = @author_id
|
|
AND s.client_request_id = @client_request_id;
|
|
|
|
-- name: BindCommentTaskSupplement :one
|
|
-- 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.
|
|
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 = @task_id
|
|
AND t.issue_id = @issue_id
|
|
AND t.agent_id = @agent_id
|
|
AND r.workspace_id = @workspace_id
|
|
AND t.status = 'running'
|
|
AND cap.capability = 'task-supplement-v1'
|
|
AND r.provider IN ('codex', 'claude', 'grok', 'cursor', 'kiro')
|
|
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, @workspace_id, t.issue_id, @comment_id, @author_id,
|
|
@client_request_id, 'pending'
|
|
FROM locked_task t
|
|
RETURNING *
|
|
)
|
|
SELECT inserted.*, locked_task.runtime_id
|
|
FROM inserted
|
|
JOIN locked_task ON locked_task.id = inserted.task_id;
|
|
|
|
-- name: GetTaskSupplementForRun :one
|
|
SELECT * FROM task_supplement
|
|
WHERE comment_id = @comment_id AND task_id = @task_id AND workspace_id = @workspace_id;
|
|
|
|
-- name: CommentHasTaskSupplement :one
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM task_supplement
|
|
WHERE comment_id = @comment_id AND workspace_id = @workspace_id
|
|
) AS bound;
|
|
|
|
-- name: GetTaskSupplementCapability :one
|
|
SELECT * FROM task_supplement_capability WHERE task_id = @task_id;
|
|
|
|
-- name: ListTaskSupplementsByCommentIDs :many
|
|
-- One comment may steer several runs; receipts carry the run's agent so a
|
|
-- client can say whose turn received it.
|
|
SELECT s.*, t.agent_id
|
|
FROM task_supplement s
|
|
LEFT JOIN agent_task_queue t ON t.id = s.task_id
|
|
WHERE s.workspace_id = @workspace_id
|
|
AND s.comment_id = ANY(@comment_ids::uuid[])
|
|
ORDER BY s.created_at, s.task_id;
|
|
|
|
-- 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 = @workspace_id
|
|
AND cap.task_id = ANY(@task_ids::uuid[])
|
|
GROUP BY cap.task_id, cap.capability;
|
|
|
|
-- name: SettleTerminalTaskSupplements :execrows
|
|
-- 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.
|
|
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(@task_ids::uuid[])
|
|
AND task.status IN ('completed', 'failed', 'cancelled')
|
|
AND supplement.status IN ('pending', 'delivering');
|
|
|
|
-- 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 = @task_id
|
|
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.*
|
|
)
|
|
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;
|
|
|
|
-- name: AckTaskSupplementDelivered :one
|
|
-- The provider can accept input before completion while its HTTP acknowledgement
|
|
-- arrives afterward. Serialize with settlement and preserve that successful write.
|
|
WITH task AS MATERIALIZED (
|
|
SELECT id FROM agent_task_queue WHERE id = @task_id 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 = @comment_id
|
|
AND (s.status IN ('delivering', 'delivered')
|
|
OR (s.status = 'failed' AND s.failure_reason = 'turn_ended' AND s.attempt_count > 0))
|
|
RETURNING s.*;
|
|
|
|
-- name: AckTaskSupplementFailed :one
|
|
UPDATE task_supplement
|
|
SET status = 'failed',
|
|
failure_reason = @failure_reason,
|
|
updated_at = now()
|
|
WHERE task_id = @task_id
|
|
AND comment_id = @comment_id
|
|
AND status = 'delivering'
|
|
RETURNING *;
|
|
|
|
-- name: RetryTaskSupplement :one
|
|
WITH comment AS MATERIALIZED (
|
|
SELECT id FROM comment
|
|
WHERE id = @comment_id AND workspace_id = @workspace_id 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 = @task_id
|
|
AND agent_task_queue.issue_id = @issue_id
|
|
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 = @comment_id
|
|
AND s.workspace_id = @workspace_id
|
|
AND s.status = 'failed'
|
|
RETURNING s.*;
|
|
|
|
-- name: LockTaskSupplementsByComment :many
|
|
SELECT * FROM task_supplement
|
|
WHERE comment_id = @comment_id AND workspace_id = @workspace_id
|
|
FOR UPDATE;
|
|
|
|
-- name: DeleteTaskSupplementByComment :exec
|
|
DELETE FROM task_supplement
|
|
WHERE comment_id = @comment_id AND workspace_id = @workspace_id;
|