mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
Add the open-connector business integration: user-owned OAuth and API-key connections mirrored in user_connector_connection, a DB-backed OAuth state table so callbacks survive restarts and replicas, and a per-task runtime token minted at enqueue with allowedConnections pinned to the agent owner's connections in the agent's service allowlist. The token id is stored in agent_task_queue.runtime_connector_handles and released when the task reaches a terminal state; a reaper entry point releases stale handles and expired OAuth states. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01XqqpKySn2iWtXriK3Gsgak
323 lines
22 KiB
SQL
323 lines
22 KiB
SQL
-- name: CreateIssueWakeup :one
|
|
INSERT INTO issue_wakeup(id,workspace_id,issue_id,agent_id,created_by,source_task_id,parent_comment_id,instruction,kind,mode,event_types,filter_agent_id,filter_task_id,filter_actor_type,filter_actor_id,interval_seconds,cron_expression,timezone,next_fire_at,expires_at,expiry_seconds,on_timeout,condition,max_fires)
|
|
VALUES(@id,@workspace_id,@issue_id,@agent_id,@created_by,sqlc.narg(source_task_id),sqlc.narg(parent_comment_id),@instruction,@kind,@mode,@event_types,sqlc.narg(filter_agent_id),sqlc.narg(filter_task_id),sqlc.narg(filter_actor_type),sqlc.narg(filter_actor_id),sqlc.narg(interval_seconds),sqlc.narg(cron_expression),@timezone,sqlc.narg(next_fire_at),sqlc.narg(expires_at),sqlc.narg(expiry_seconds),sqlc.narg(on_timeout),sqlc.narg(condition),sqlc.narg(max_fires)) RETURNING *;
|
|
-- name: ListIssueWakeups :many
|
|
SELECT w.id,w.workspace_id,w.issue_id,w.agent_id,w.created_by,w.source_task_id,w.parent_comment_id,w.instruction,
|
|
w.kind,w.mode,w.event_types,w.filter_actor_type,
|
|
(CASE WHEN actor_agent.id IS NOT NULL OR actor_member.user_id IS NOT NULL THEN w.filter_actor_id END)::uuid AS filter_actor_id,
|
|
COALESCE(actor_agent.name,actor_user.name,'')::text AS filter_actor_name,
|
|
(CASE WHEN source.id IS NOT NULL THEN w.filter_agent_id END)::uuid AS filter_agent_id,
|
|
(CASE WHEN EXISTS(SELECT 1 FROM agent_task_queue ft JOIN agent fa ON fa.id=ft.agent_id AND fa.workspace_id=w.workspace_id
|
|
WHERE ft.id=w.filter_task_id AND ft.issue_id=w.issue_id AND fa.id=ANY(@agent_ids::uuid[])) THEN w.filter_task_id END)::uuid AS filter_task_id,
|
|
w.interval_seconds,w.cron_expression,w.timezone,w.next_fire_at,w.enabled,w.disabled_at,w.revision,
|
|
w.last_task_id,w.last_error,w.created_at,w.updated_at,a.name AS agent_name,source.name AS filter_agent_name,t.status AS last_task_status,
|
|
w.expires_at,w.expiry_seconds,w.on_timeout,w.timed_out_at,
|
|
w.condition,w.max_fires,w.fire_count,w.paused_reason,
|
|
(w.source_task_id IS NOT NULL)::bool AS created_by_agent,creator.name AS created_by_name,
|
|
(CASE WHEN creator_agent.id IS NOT NULL THEN creator_agent.id END)::uuid AS source_agent_id,creator_agent.name AS source_agent_name
|
|
FROM issue_wakeup w JOIN agent a ON a.id=w.agent_id AND a.workspace_id=w.workspace_id
|
|
LEFT JOIN "user" creator ON creator.id=w.created_by
|
|
LEFT JOIN agent_task_queue creator_task ON creator_task.id=w.source_task_id
|
|
LEFT JOIN agent creator_agent ON creator_agent.id=creator_task.agent_id AND creator_agent.workspace_id=w.workspace_id AND creator_agent.id=ANY(@agent_ids::uuid[])
|
|
LEFT JOIN agent actor_agent ON w.filter_actor_type='agent' AND actor_agent.id=w.filter_actor_id AND actor_agent.workspace_id=w.workspace_id AND actor_agent.id=ANY(@agent_ids::uuid[])
|
|
LEFT JOIN member actor_member ON w.filter_actor_type='member' AND actor_member.user_id=w.filter_actor_id AND actor_member.workspace_id=w.workspace_id
|
|
LEFT JOIN "user" actor_user ON actor_user.id=actor_member.user_id
|
|
LEFT JOIN agent source ON source.id=w.filter_agent_id AND source.workspace_id=w.workspace_id AND source.id=ANY(@agent_ids::uuid[])
|
|
LEFT JOIN agent_task_queue t ON t.id=w.last_task_id AND t.issue_id=w.issue_id AND t.agent_id=w.agent_id
|
|
WHERE w.workspace_id= @workspace_id AND w.issue_id= @issue_id ORDER BY w.created_at,w.id;
|
|
|
|
-- name: ListWorkspaceWakeupSummaryRows :many
|
|
-- No prompts/history; at most three previews per issue plus exact counts.
|
|
WITH ranked AS (
|
|
SELECT w.issue_id,w.id,w.agent_id,a.name AS agent_name,w.kind,w.mode,w.event_types,w.filter_actor_type,
|
|
(CASE WHEN actor_agent.id IS NOT NULL OR actor_member.user_id IS NOT NULL THEN w.filter_actor_id END)::uuid AS filter_actor_id,
|
|
COALESCE(actor_agent.name,actor_user.name,'')::text AS filter_actor_name,
|
|
(CASE WHEN EXISTS(SELECT 1 FROM agent_task_queue ft JOIN agent fa ON fa.id=ft.agent_id AND fa.workspace_id=w.workspace_id
|
|
WHERE ft.id=w.filter_task_id AND ft.issue_id=w.issue_id AND fa.id=ANY(@agent_ids::uuid[])) THEN w.filter_task_id END)::uuid AS filter_task_id,
|
|
source.name AS filter_agent_name,w.interval_seconds,w.cron_expression,w.timezone,w.next_fire_at,w.condition,
|
|
count(*) OVER(PARTITION BY w.issue_id) AS active_count,
|
|
count(*) FILTER(WHERE w.kind='event') OVER(PARTITION BY w.issue_id) AS event_count,
|
|
row_number() OVER(PARTITION BY w.issue_id ORDER BY w.next_fire_at NULLS LAST,w.created_at,w.id) AS rank
|
|
FROM issue_wakeup w
|
|
JOIN issue i ON i.id=w.issue_id AND i.workspace_id=w.workspace_id
|
|
JOIN agent a ON a.id=w.agent_id AND a.workspace_id=w.workspace_id
|
|
LEFT JOIN agent actor_agent ON w.filter_actor_type='agent' AND actor_agent.id=w.filter_actor_id AND actor_agent.workspace_id=w.workspace_id AND actor_agent.id=ANY(@agent_ids::uuid[])
|
|
LEFT JOIN member actor_member ON w.filter_actor_type='member' AND actor_member.user_id=w.filter_actor_id AND actor_member.workspace_id=w.workspace_id
|
|
LEFT JOIN "user" actor_user ON actor_user.id=actor_member.user_id
|
|
LEFT JOIN agent source ON source.id=w.filter_agent_id AND source.workspace_id=w.workspace_id AND source.id=ANY(@agent_ids::uuid[])
|
|
WHERE w.workspace_id= @workspace_id AND w.enabled
|
|
AND i.status NOT IN ('done','cancelled')
|
|
AND NOT EXISTS(SELECT 1 FROM issue_status s WHERE s.workspace_id=i.workspace_id AND s.key=i.status AND s.category IN ('done','closed'))
|
|
)
|
|
SELECT issue_id,id,agent_id,agent_name,kind,mode,event_types,filter_actor_type,filter_actor_id,filter_actor_name,filter_task_id,filter_agent_name,interval_seconds,cron_expression,timezone,next_fire_at,condition,active_count,event_count
|
|
FROM ranked WHERE rank<=3 ORDER BY issue_id,rank;
|
|
-- name: GetIssueWakeup :one
|
|
SELECT * FROM issue_wakeup WHERE id= @id AND workspace_id= @workspace_id;
|
|
-- name: LockIssueWakeup :one
|
|
SELECT * FROM issue_wakeup WHERE id= @id FOR UPDATE;
|
|
-- name: LockWakeupIssue :one
|
|
SELECT * FROM issue WHERE id= @id FOR NO KEY UPDATE;
|
|
-- name: LockWakeupSourceTask :one
|
|
SELECT * FROM agent_task_queue WHERE id= @id AND issue_id= @issue_id FOR UPDATE NOWAIT;
|
|
-- name: CancelUnstartedWakeupTasks :many
|
|
UPDATE agent_task_queue SET status='cancelled',completed_at=now(),error='Wakeup disabled or updated'
|
|
WHERE context->>'wakeup_id'= @wakeup_id::text AND status IN ('queued','deferred') AND started_at IS NULL RETURNING *;
|
|
-- name: DisableIssueWakeups :exec
|
|
-- Closing wins over every subscription on the issue; reopening does not rearm.
|
|
-- A system rule only rests while its issue is closed and resumes on reopen.
|
|
UPDATE issue_wakeup SET enabled=false,disabled_at=clock_timestamp(),updated_at=clock_timestamp()
|
|
WHERE issue_id= @issue_id AND disabled_at IS NULL AND system_rule IS NULL;
|
|
-- name: CancelUnstartedIssueWakeupTasks :many
|
|
UPDATE agent_task_queue SET status='cancelled',completed_at=now(),error='Issue closed; wakeup disabled'
|
|
WHERE issue_id= @issue_id AND context->>'wakeup_id' IS NOT NULL AND status IN ('queued','deferred') AND started_at IS NULL RETURNING *;
|
|
-- name: ListInputWakeups :many
|
|
-- Scheduler lane 1: rules with unprocessed inputs (subscribed events,
|
|
-- condition hints, "wake now"). Deliberately not filtered by enabled: a once
|
|
-- rule that stopped still delivers the inputs it captured before stopping.
|
|
-- The subquery reads only pending rows (issue_wakeup_receipt_pending_idx).
|
|
-- Rules already visited in this pass are skipped.
|
|
SELECT w.* FROM issue_wakeup w
|
|
WHERE w.id IN (SELECT r.wakeup_id FROM issue_wakeup_receipt r WHERE r.processed_at IS NULL)
|
|
AND (sqlc.narg('workspace_ids')::uuid[] IS NULL OR w.workspace_id = ANY(sqlc.narg('workspace_ids')::uuid[]))
|
|
AND NOT (w.id = ANY(@skip_ids::uuid[]))
|
|
ORDER BY w.updated_at,w.id LIMIT @row_limit;
|
|
-- name: ListDueWakeups :many
|
|
-- Scheduler lane 2: schedules, deadlines and condition checks that are due.
|
|
-- Rules already visited in this pass are skipped.
|
|
WITH candidates AS (
|
|
SELECT id FROM issue_wakeup WHERE enabled AND kind<>'event' AND next_fire_at<=now()
|
|
UNION
|
|
SELECT id FROM issue_wakeup WHERE enabled AND expires_at<=now()
|
|
UNION
|
|
SELECT id FROM issue_wakeup WHERE enabled AND condition IS NOT NULL AND next_fire_at<=now()
|
|
)
|
|
SELECT w.* FROM candidates c JOIN issue_wakeup w ON w.id=c.id
|
|
WHERE (sqlc.narg('workspace_ids')::uuid[] IS NULL OR w.workspace_id = ANY(sqlc.narg('workspace_ids')::uuid[]))
|
|
AND NOT (w.id = ANY(@skip_ids::uuid[]))
|
|
ORDER BY w.updated_at,w.id LIMIT @row_limit;
|
|
-- name: ListPendingWakeupInputGroups :many
|
|
-- Scheduler observability over unprocessed inputs, grouped per rule and by
|
|
-- whether a run still reserves them. Dispatch deliberately keeps some inputs
|
|
-- pending while a run holds them; these flags mirror its checks:
|
|
-- reserved: a claim reserved them for a run that has not started and has
|
|
-- not ended (takenReceipts). A run that ended without starting
|
|
-- releases them, and one that started has them consumed on the
|
|
-- next dispatch, so both are backlog.
|
|
-- own_claimed: the rule's own run is claimed (FindPendingWakeupTask).
|
|
-- user_waiting: a queued run of the rule's agent, run as its creator, will
|
|
-- take them along (hasWaitingRun). Platform rules wait for the
|
|
-- issue's current target instead; the caller resolves that.
|
|
-- Oldest groups first, one page at a time: the caller pages with the last
|
|
-- group's (oldest, wakeup_id, reserved) until a short page, so no group is
|
|
-- left unread however many there are.
|
|
WITH groups AS (
|
|
SELECT w.id AS wakeup_id, w.issue_id, (w.system_rule IS NOT NULL)::bool AS system_rule,
|
|
(r.task_id IS NOT NULL AND EXISTS (SELECT 1 FROM agent_task_queue t WHERE t.id=r.task_id
|
|
AND t.started_at IS NULL AND t.status NOT IN ('running','completed','failed','cancelled')))::bool AS reserved,
|
|
EXISTS (SELECT 1 FROM agent_task_queue t WHERE t.context->>'wakeup_id'=w.id::text AND t.status='dispatched')::bool AS own_claimed,
|
|
(w.system_rule IS NULL AND EXISTS (SELECT 1 FROM agent_task_queue t WHERE t.issue_id=w.issue_id AND t.status='queued'
|
|
AND t.agent_id=w.agent_id AND t.originator_user_id=w.created_by))::bool AS user_waiting,
|
|
min(r.created_at)::timestamptz AS oldest, count(*)::int AS inputs
|
|
FROM issue_wakeup_receipt r JOIN issue_wakeup w ON w.id=r.wakeup_id
|
|
WHERE r.processed_at IS NULL
|
|
AND (sqlc.narg('workspace_ids')::uuid[] IS NULL OR w.workspace_id = ANY(sqlc.narg('workspace_ids')::uuid[]))
|
|
GROUP BY 1,2,3,4,5,6
|
|
)
|
|
SELECT g.wakeup_id, g.issue_id, g.system_rule, g.reserved, g.own_claimed, g.user_waiting, g.oldest, g.inputs FROM groups g
|
|
WHERE sqlc.narg('after_oldest')::timestamptz IS NULL
|
|
OR (g.oldest, g.wakeup_id, g.reserved) > (sqlc.narg('after_oldest')::timestamptz, @after_wakeup_id::uuid, @after_reserved::bool)
|
|
ORDER BY g.oldest, g.wakeup_id, g.reserved LIMIT @page_size;
|
|
-- name: ListPendingWakeupReceipts :many
|
|
SELECT * FROM issue_wakeup_receipt WHERE wakeup_id= @wakeup_id AND revision= @revision AND processed_at IS NULL ORDER BY created_at,id LIMIT 100 FOR UPDATE;
|
|
|
|
-- name: DeleteExpiredWakeupReceipts :execrows
|
|
-- Pending inputs are never expired. Bound work and avoid waiting on dispatch.
|
|
-- MATERIALIZED evaluates the batch once. As an IN subquery, a nested-loop plan
|
|
-- rescans it per outer row, skips the rows this DELETE already removed, and
|
|
-- slides the LIMIT window until every expired row is gone.
|
|
WITH batch AS MATERIALIZED (
|
|
SELECT expired.id FROM issue_wakeup_receipt expired WHERE expired.processed_at < @cutoff
|
|
ORDER BY expired.processed_at,expired.id LIMIT 1000 FOR UPDATE SKIP LOCKED
|
|
)
|
|
DELETE FROM issue_wakeup_receipt r USING batch WHERE r.id=batch.id;
|
|
-- name: RecordWakeupReceipt :one
|
|
INSERT INTO issue_wakeup_receipt(id,wakeup_id,revision,event_key,event_type,payload)
|
|
VALUES(@id,@wakeup_id,@revision,@event_key,@event_type,@payload)
|
|
ON CONFLICT(wakeup_id,revision,event_key) DO UPDATE SET event_key=EXCLUDED.event_key RETURNING *;
|
|
-- name: ConsumeWakeupReceipts :exec
|
|
UPDATE issue_wakeup_receipt SET task_id=sqlc.narg(task_id),processed_at=now() WHERE id=ANY(@ids::uuid[]);
|
|
-- name: DiscardWakeupReceipts :exec
|
|
UPDATE issue_wakeup_receipt SET processed_at=now() WHERE wakeup_id= @id AND processed_at IS NULL;
|
|
-- name: AdvanceIssueWakeup :exec
|
|
UPDATE issue_wakeup SET enabled= @enabled,next_fire_at=sqlc.narg(next_fire_at),last_task_id=COALESCE(sqlc.narg(last_task_id),last_task_id),last_error=sqlc.narg(last_error),updated_at=clock_timestamp() WHERE id= @id;
|
|
-- name: FindPendingWakeupTask :one
|
|
SELECT * FROM agent_task_queue WHERE context->>'wakeup_id'= @wakeup_id::text AND status IN ('queued','dispatched') ORDER BY created_at LIMIT 1 FOR UPDATE;
|
|
|
|
-- name: CreateWakeupTask :one
|
|
-- 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).
|
|
-- head_sha stamps the commit under review into the task's context JSONB so the
|
|
-- reviewer-loop dedup (HasPendingTaskForIssueAndAgent) can tell a pending run
|
|
-- against an OLD head apart from a fresh request against a NEW head (TEN-356).
|
|
-- Empty/absent head_sha leaves context NULL, preserving pre-TEN-356 behavior for
|
|
-- issues with no linked PR. Issue-linked tasks never hit quick-create context
|
|
-- parsing (parseQuickCreateContext short-circuits on IssueID.Valid), so this
|
|
-- key rides harmlessly alongside.
|
|
-- id is minted by the application as a UUIDv7 (pkg/dbid) so consecutive
|
|
-- enqueues cluster in a narrow contiguous primary-key range instead of
|
|
-- scattering across the B-tree. On a table with existing v4 ids, that range
|
|
-- is not necessarily the tree's right edge.
|
|
-- COALESCE keeps the column's gen_random_uuid() default reachable, so a caller
|
|
-- that passes no id still inserts — it just gets a random v4, exactly as before.
|
|
-- The same pattern is used by every INSERT listed in pkg/dbid's write table.
|
|
INSERT INTO agent_task_queue (
|
|
agent_id, runtime_id, issue_id, status, priority, trigger_comment_id,
|
|
coalesced_comment_ids, trigger_summary, force_fresh_session, is_leader_task, handoff_note,
|
|
squad_id, context, originator_user_id, accountable_user_id, runtime_mcp_overlay, runtime_connected_apps, runtime_connector_handles,
|
|
originator_source, delegated_from_task_id, rule_version_id, rerun_of_task_id, trigger_evidence_kind, trigger_evidence_ref_id,
|
|
workflow_step, id
|
|
)
|
|
SELECT
|
|
$1, $2, $3, 'queued', $4, sqlc.narg(trigger_comment_id),
|
|
COALESCE(sqlc.narg(coalesced_comment_ids)::uuid[], '{}'),
|
|
sqlc.narg(trigger_summary),
|
|
COALESCE(sqlc.narg('force_fresh_session')::boolean, FALSE),
|
|
COALESCE(sqlc.narg('is_leader_task')::boolean, FALSE),
|
|
sqlc.narg(handoff_note),
|
|
sqlc.narg(squad_id),
|
|
CASE
|
|
WHEN COALESCE(sqlc.narg('head_sha')::text, '') <> ''
|
|
THEN jsonb_build_object('head_sha', sqlc.narg('head_sha')::text)
|
|
ELSE '{}'::jsonb
|
|
END || @wakeup_context::jsonb,
|
|
sqlc.narg(originator_user_id),
|
|
sqlc.narg(accountable_user_id),
|
|
sqlc.narg(runtime_mcp_overlay),
|
|
sqlc.narg(runtime_connected_apps), sqlc.narg(runtime_connector_handles),
|
|
sqlc.narg(originator_source),
|
|
sqlc.narg(delegated_from_task_id),
|
|
sqlc.narg(rule_version_id),
|
|
sqlc.narg(rerun_of_task_id),
|
|
sqlc.narg(trigger_evidence_kind),
|
|
sqlc.narg(trigger_evidence_ref_id),
|
|
-- The workflow step this run works on (MUL-7420). A wakeup continues the
|
|
-- run that registered it, so it keeps that run's step: a status change it
|
|
-- makes after someone else moved the issue is refused like the run's own
|
|
-- would be. A wakeup a member registered starts from the issue's status.
|
|
COALESCE(
|
|
(SELECT s.workflow_step FROM agent_task_queue s WHERE s.id = sqlc.narg(delegated_from_task_id) AND s.issue_id = $3),
|
|
(SELECT i.status FROM issue i WHERE i.id = $3)
|
|
),
|
|
COALESCE(sqlc.narg('id')::uuid, gen_random_uuid())
|
|
WHERE lock_task_owner_rows($1, $3, $2)
|
|
RETURNING *;
|
|
|
|
-- name: LocklessWakeup :one
|
|
SELECT * FROM issue_wakeup WHERE id= @id;
|
|
|
|
-- name: ReplaceWakeupEvidence :one
|
|
UPDATE agent_task_queue SET handoff_note=sqlc.narg(handoff_note), context=COALESCE(context,'{}'::jsonb) || jsonb_build_object('wakeup_evidence', @wakeup_evidence::jsonb) WHERE id= @id AND status='queued' RETURNING *;
|
|
-- name: NoteWakeupFailure :exec
|
|
UPDATE issue_wakeup SET last_error=sqlc.narg(last_error),updated_at=clock_timestamp() WHERE id= @id;
|
|
-- name: TouchWakeupDispatch :exec
|
|
-- Move blocked configurations to the back of the scan so one noisy issue
|
|
-- cannot monopolize the bounded batch.
|
|
UPDATE issue_wakeup SET updated_at=clock_timestamp() WHERE id= @id;
|
|
|
|
-- name: MarkWakeupTimedOut :exec
|
|
-- The deadline ended the rule. disabled_at stays NULL: a timeout run created in
|
|
-- the same transaction must remain claimable, and the rule reads as timed out,
|
|
-- not as turned off by a person.
|
|
UPDATE issue_wakeup SET enabled=false,next_fire_at=NULL,timed_out_at=clock_timestamp(),updated_at=clock_timestamp() WHERE id= @id;
|
|
|
|
-- name: SetWakeupConditionState :exec
|
|
UPDATE issue_wakeup SET condition_state= @condition_state,next_fire_at=sqlc.narg(next_fire_at),updated_at=clock_timestamp() WHERE id= @id;
|
|
|
|
-- name: PauseIssueWakeup :exec
|
|
-- The platform stopped a rule. A detected loop or burst also sets disabled_at
|
|
-- so queued runs of this rule can no longer be claimed; hitting the cap does
|
|
-- not, because the run that reached it is legitimate.
|
|
UPDATE issue_wakeup SET enabled=false,next_fire_at=NULL,paused_reason= @paused_reason,
|
|
disabled_at=CASE WHEN @block_runs::bool THEN COALESCE(disabled_at,clock_timestamp()) ELSE disabled_at END,
|
|
updated_at=clock_timestamp() WHERE id= @id;
|
|
|
|
-- name: CountWakeupFires :exec
|
|
UPDATE issue_wakeup SET fire_count=fire_count+1 WHERE id= @id;
|
|
|
|
-- name: CountRecentWakeupTasks :one
|
|
SELECT count(*) FROM agent_task_queue WHERE context->>'wakeup_id'= @wakeup_id::text AND issue_id= @issue_id AND created_at> @since;
|
|
|
|
-- name: ListWakeupChains :many
|
|
-- Rules whose runs produced the source events, including rules further up
|
|
-- each run's own trigger chain.
|
|
SELECT id,COALESCE(context->>'wakeup_id','')::text AS wakeup_id,COALESCE(context->'wakeup_chain','[]'::jsonb)::jsonb AS chain
|
|
FROM agent_task_queue WHERE id=ANY(@ids::uuid[]);
|
|
|
|
-- name: ListWakeupRuns :many
|
|
-- The rule's latest runs with what triggered each one, a check-in note when
|
|
-- the run ended silently, and whether it left a comment.
|
|
SELECT t.id,t.status,t.created_at,t.started_at,t.completed_at,
|
|
COALESCE(t.context->'wakeup_checkin'->>'note','')::text AS checkin_note,
|
|
COALESCE((SELECT array_agg(DISTINCT f->>'event_type') FROM jsonb_array_elements(CASE WHEN jsonb_typeof(t.context->'wakeup_evidence'->'facts')='array' THEN t.context->'wakeup_evidence'->'facts' ELSE '[]'::jsonb END) f),'{}')::text[] AS triggers,
|
|
EXISTS(SELECT 1 FROM comment c WHERE c.issue_id=t.issue_id AND c.source_task_id=t.id)::bool AS commented
|
|
FROM agent_task_queue t WHERE t.context->>'wakeup_id'= @wakeup_id::text AND t.issue_id= @issue_id
|
|
ORDER BY t.created_at DESC,t.id DESC LIMIT 10;
|
|
|
|
-- name: DeleteIssueWakeupReceipts :exec
|
|
DELETE FROM issue_wakeup_receipt WHERE wakeup_id= @id;
|
|
|
|
-- name: DeleteIssueWakeup :exec
|
|
DELETE FROM issue_wakeup WHERE id= @id AND issue_id= @issue_id;
|
|
|
|
-- name: ListPausedWakeupIssues :many
|
|
-- Rules the platform paused on open issues, for board cues and the
|
|
-- workspace banner. Access follows shared issue visibility.
|
|
SELECT w.issue_id,w.id,w.agent_id,w.paused_reason
|
|
FROM issue_wakeup w JOIN issue i ON i.id=w.issue_id AND i.workspace_id=w.workspace_id
|
|
WHERE w.workspace_id= @workspace_id AND NOT w.enabled AND w.paused_reason IN ('loop','rate','max_fires')
|
|
AND i.status NOT IN ('done','cancelled')
|
|
AND NOT EXISTS(SELECT 1 FROM issue_status s WHERE s.workspace_id=i.workspace_id AND s.key=i.status AND s.category IN ('done','closed'))
|
|
ORDER BY w.updated_at DESC LIMIT 200;
|
|
|
|
-- name: FindWaitingIssueRun :one
|
|
-- A run of the agent on the issue that has not been claimed and runs as this
|
|
-- person. A rule that fires meanwhile keeps its inputs for that run instead
|
|
-- of queuing another.
|
|
SELECT id FROM agent_task_queue WHERE issue_id= @issue_id AND agent_id= @agent_id AND status='queued'
|
|
AND originator_user_id= @originator_user_id::uuid ORDER BY created_at,id LIMIT 1;
|
|
|
|
-- name: ListWaitingWakeups :many
|
|
-- Rules on the issue with inputs a run of this agent can take along: the
|
|
-- agent's own rules and the platform's. Read without locks; each one is
|
|
-- locked and checked again before it joins.
|
|
SELECT w.id FROM issue_wakeup w
|
|
WHERE w.issue_id= @issue_id AND (w.agent_id= @agent_id OR w.system_rule IS NOT NULL) AND w.disabled_at IS NULL
|
|
AND EXISTS(SELECT 1 FROM issue_wakeup_receipt r WHERE r.wakeup_id=w.id AND r.revision=w.revision AND r.processed_at IS NULL
|
|
AND (w.condition IS NULL OR r.event_type IN ('condition.met','wakeup.timeout','wakeup.manual')))
|
|
ORDER BY w.created_at,w.id;
|
|
|
|
-- name: TryLockIssueWakeup :one
|
|
-- A rule another writer holds is skipped; it keeps its inputs.
|
|
SELECT * FROM issue_wakeup WHERE id= @id FOR UPDATE SKIP LOCKED;
|
|
|
|
-- name: ReserveWakeupReceipts :exec
|
|
-- A run being claimed takes these inputs along. They stay pending: they count
|
|
-- as handled once that run starts, and go back to the rule if it never does.
|
|
UPDATE issue_wakeup_receipt SET task_id= @task_id WHERE id=ANY(@ids::uuid[]) AND processed_at IS NULL;
|
|
|
|
-- name: ReleaseWakeupReceipts :exec
|
|
UPDATE issue_wakeup_receipt SET task_id=NULL WHERE id=ANY(@ids::uuid[]) AND processed_at IS NULL;
|
|
|
|
-- name: SetClaimedTaskContext :one
|
|
-- Only the claim being answered may change its task's context.
|
|
UPDATE agent_task_queue SET context= @context::jsonb
|
|
WHERE id= @id AND status='dispatched' AND dispatched_at= @dispatched_at RETURNING *;
|
|
|
|
-- name: CountActiveIssueRunsOfAgent :one
|
|
-- How many of these runs are the agent's own runs on this issue that have
|
|
-- not finished: the run that made a change still knows about it.
|
|
SELECT count(*) FROM agent_task_queue WHERE id=ANY(@ids::uuid[]) AND agent_id= @agent_id AND issue_id= @issue_id
|
|
AND status IN ('dispatched','running','waiting_local_directory');
|