Files
bcf40040e0 MUL-8233: fix Pi cancellation and guard unconfirmed cleanup (#9557)
* 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]>
2026-10-27 12:37:13 +08:00

188 lines
7.3 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: task_cleanup.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const clearTaskProcessCleanupPending = `-- name: ClearTaskProcessCleanupPending :exec
UPDATE agent_task_queue
SET result = result - 'process_cleanup_pending'
WHERE id = $1 AND status IN ('cancelled', 'failed')
AND result->>'process_cleanup_pending' = 'true'
`
func (q *Queries) ClearTaskProcessCleanupPending(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, clearTaskProcessCleanupPending, id)
return err
}
const failCancelledTaskProcessCleanup = `-- name: FailCancelledTaskProcessCleanup :one
UPDATE agent_task_queue
SET status = 'failed',
error = concat_ws('; ', NULLIF(error, ''), $1::text),
failure_reason = 'cleanup_unconfirmed',
completed_at = now(),
chat_finalize_deferred_at = NULL
WHERE id = $2 AND status = 'cancelled'
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, workflow_step
`
type FailCancelledTaskProcessCleanupParams struct {
ErrorMessage string `json:"error_message"`
ID pgtype.UUID `json:"id"`
}
// The daemon's cancellation acknowledgement can discover that local work did
// NOT stop. Preserve the cancelled run's metadata but surface this as failure.
// The status CAS makes replay harmless and leaves completed/failed rows alone.
func (q *Queries) FailCancelledTaskProcessCleanup(ctx context.Context, arg FailCancelledTaskProcessCleanupParams) (AgentTaskQueue, error) {
row := q.db.QueryRow(ctx, failCancelledTaskProcessCleanup, arg.ErrorMessage, 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,
&i.WorkflowStep,
)
return i, err
}
const markPiTaskProcessCleanupPending = `-- name: MarkPiTaskProcessCleanupPending :exec
UPDATE agent_task_queue task
SET result = COALESCE(task.result, '{}'::jsonb) || '{"process_cleanup_pending":true}'::jsonb
WHERE task.id = $1 AND task.status = 'cancelled' AND task.started_at IS NOT NULL
AND task.result->>'process_cleanup_capability' = 'pi-process-cleanup-v1'
AND EXISTS (SELECT 1 FROM agent_runtime runtime WHERE runtime.id = task.runtime_id AND runtime.provider IN ('pi', 'omp'))
`
// Cancellation makes the task terminal before the daemon finishes stopping it.
// Only a run that negotiated confirmation at start holds a fence until ack.
func (q *Queries) MarkPiTaskProcessCleanupPending(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, markPiTaskProcessCleanupPending, id)
return err
}
const taskBlockedByProcessCleanup = `-- name: TaskBlockedByProcessCleanup :one
SELECT EXISTS (
SELECT 1
FROM agent_task_queue candidate
JOIN LATERAL (
SELECT CASE WHEN unsafe.status = 'cancelled'
AND unsafe.result->>'process_cleanup_pending' = 'true'
THEN unsafe.completed_at + interval '30 seconds'
ELSE COALESCE(unsafe.completed_at, unsafe.created_at)
END AS completed_at
FROM agent_task_queue unsafe
WHERE unsafe.agent_id = candidate.agent_id
AND unsafe.runtime_id = candidate.runtime_id
AND (unsafe.failure_reason = 'cleanup_unconfirmed'
OR (unsafe.status = 'cancelled' AND unsafe.result->>'process_cleanup_pending' = 'true'
AND unsafe.completed_at <= now() - interval '30 seconds'))
ORDER BY unsafe.completed_at DESC, unsafe.id DESC
LIMIT 1
) unsafe ON true
WHERE candidate.id = $1
AND candidate.status IN ('queued', 'dispatched', 'waiting_local_directory')
AND NOT EXISTS (
SELECT 1 FROM agent_task_queue manual
WHERE manual.agent_id = candidate.agent_id
AND manual.runtime_id = candidate.runtime_id
AND manual.rerun_of_task_id IS NOT NULL
AND manual.retry_of_task_id IS NULL
AND manual.created_at > unsafe.completed_at
AND (manual.id = candidate.id OR manual.status = 'completed')
)
)
`
// A cleanup failure fences this agent on this runtime, including already queued
// work and future scheduled runs. Only a NEW explicit manual rerun can cross
// the fence; its successful completion allows later automated work again.
// Existing failure and result fields are the durable safety markers.
func (q *Queries) TaskBlockedByProcessCleanup(ctx context.Context, taskID pgtype.UUID) (bool, error) {
row := q.db.QueryRow(ctx, taskBlockedByProcessCleanup, taskID)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const taskWaitingForProcessCleanup = `-- name: TaskWaitingForProcessCleanup :one
SELECT EXISTS (
SELECT 1 FROM agent_task_queue candidate
JOIN agent_task_queue stopping ON stopping.agent_id = candidate.agent_id AND stopping.runtime_id = candidate.runtime_id
WHERE candidate.id = $1 AND candidate.status IN ('dispatched', 'waiting_local_directory')
AND stopping.status = 'cancelled' AND stopping.result->>'process_cleanup_pending' = 'true'
AND stopping.completed_at > now() - interval '30 seconds'
)
`
func (q *Queries) TaskWaitingForProcessCleanup(ctx context.Context, id pgtype.UUID) (bool, error) {
row := q.db.QueryRow(ctx, taskWaitingForProcessCleanup, id)
var exists bool
err := row.Scan(&exists)
return exists, err
}