mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
* feat(wakeups): add rule deadlines and expose the child-done system rule (MUL-7680)
Wakeups can now end: an absolute deadline (expires_at) or a relative wait
(expires_in_seconds) that restarts when the rule is re-enabled. When the
deadline passes first, the scheduler ends the rule; event rules with
on_timeout=wake run the target once with a wakeup.timeout fact. A timed-out
rule keeps disabled_at NULL so its timeout run stays claimable.
The implicit "wake the parent's assignee when a stage of sub-issues
finishes" behavior is now described by GET /api/issues/{id}/system-wakeups
and can be turned off or given a supplementary instruction per issue via
PUT /api/issues/{id}/system-wakeups/child_done. Rule reads fail open to the
previous behavior.
List responses add the creator (member or agent) and the new expiry fields.
The CLI gains --expires-in, --expires-at and --on-timeout.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(issues): let members create wakeups and show the child-done system rule (MUL-7680)
The issue sidebar's Wakeups section is always available on open issues and
gains a New wakeup popover. Members pick a condition (a time, a recurring
check with an end date, someone's reply, an agent's run ending, or raw
events), the agent to wake, the instruction, how often it fires, how long
to wait and what happens on timeout. It posts the same configuration agents
create with the CLI.
The parent's child-done wake appears as a System rule with its stage,
remaining sub-issues and target, a per-issue toggle and a supplementary
instruction. Rows show when a rule ends, timed-out rules read as such, and
details name who created the rule. Rule titles wrap to two lines instead of
truncating.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* docs(wakeups): document deadlines, member-created rules and the system rule (MUL-7680)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(wakeups): platform conditions, runaway protection and check-ins (MUL-7680)
Conditions: a wakeup can now wait for a fact the platform checks itself
(a status, assignee, label or property value; sub-issues or one stage
finishing; a linked PR's checks finishing or merging; another issue's
status). Related events only prompt an early evaluation; the rule wakes
its agent when the predicate becomes true, and a repeating rule fires
again only after it turned false or its facts changed. Finished PR checks
from before registration are ignored.
Runaway protection: repeating event rules stop after max_fires runs
(default 20), a rule pauses when its causal chain passes through it a
third time without a person in between, or when it starts 12 runs in an
hour. The reason is stored and shown.
Also: silent check-ins for every/cron checks (the run then posts no
fallback comment), wake now, delete, a per-rule run history, paused-rule
lists, timeline entries for created/triggered/timed-out/paused/check-in,
and source, paused scope, 7-day runs and child-done system rows in the
workspace list. The CLI gains --until-* conditions, --max-fires and the
trigger/delete/checkin/runs commands; the brief and wakeup prompt state
the check-in exception.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(issues): record child-done transitions in the status write (MUL-7680)
The child-done system rule ran after the status write committed and gave
up on any error, so a crash, deploy or transient failure lost the
parent's wake. A trigger now records each child's move into a closed
status in the writing transaction, for every writer. The request that
wrote it processes the rows right away and a scheduler sweep retries
anything left over; claims make the two paths process a transition
once.
The rule also follows a workspace default (settings key
system_wakeup_child_done) until an issue sets its own, the per-issue
update accepts partial bodies, and each trigger adds a timeline entry
that names its system comment.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(views): wakeup conditions, history, timeline and workspace list (MUL-7680)
Issue sidebar: members can create the platform-checked conditions (a field
reaching a value, sub-issues or a stage finishing, a linked PR's CI or merge,
another issue's status) and cap how often a repeating wait fires. Rule
details show the fire count, why the platform paused a rule, the latest runs
with silent-check notes, and offer wake now and delete.
The issue header says what the issue is waiting for ("Emacs 在等 Jiayuan 回复
+2") and opens the Wakeups section. Board and list cards use the same short
sentence, or flag a paused rule. Wakeup runs read as "由唤醒触发 · <condition>"
in the execution log, transcript and on the comments they post.
The timeline shows created, triggered, timed-out, paused and check-in entries
with the rule they came from; consecutive check-ins merge, and the child-done
entry stands in for its system comment, which it can reveal.
Automation → Issue wakeups adds a source column and filter, 7-day runs, a
paused scope with a banner, the child-done system rule on each waiting parent,
and "New wakeup" with an issue picker. Settings → Issue statuses sets the
system rule's workspace default. Copy in all five languages.
The server records the watched issue's identifier in other-issue conditions
and the rule's creator in timeline snapshots, and returns board summaries with
their condition.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* docs(wakeups): document conditions, runaway protection and check-ins (MUL-7680)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(wakeups): list system rules without a revision and localize statuses (MUL-7680)
System rule rows returned revision 0, which clients reject as a rule
revision; they now return null, and a list row with an invalid revision no
longer fails the whole page. Built-in statuses in conditions and the status
picker read in the viewer's language. Adds a browser test for a condition
created from the form, the header summary, the scheduler waking the agent
and the workspace list.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(views): say why a rule paused without repeating "paused" (MUL-7680)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(i18n): use the product's task terms in wakeup copy (MUL-7680)
ja, ko and fr wakeup strings said イシュー / 이슈 / ticket; the product term
for an issue in those languages is タスク / 태스크 / tâche (and sub-tasks
accordingly), per the conventions page. French strings are rewritten for
the feminine noun.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* test(views): pin the shortcut platform in the wakeup form test (MUL-7680)
The send shortcut is primary+Enter, which is Ctrl on Linux CI runners, so
pressing Cmd+Enter only submitted on macOS.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(views): simplify the workspace wakeup list
Put the filters on one row: scope as a segmented control on the left,
source/trigger/agent and a search-as-you-type box on the right. "New
wakeup" moves to the page header, which drops the separate Search button.
Rows are single-line: the identifier sits before the title, frequency and
timezone move into the trigger tooltip, and selection, prompt edit and
the last run's transcript show on row hover or focus. The issue column
takes the remaining width and truncates, so the table fits the page.
Also fix creating from the list: the issue picker closes itself right
after reporting the selection, and that close cancelled the flow before
the form could open.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* refactor(server): run the child_done rule as a system issue wakeup
The parent-assignee wake on closed sub-issue stages was its own code path:
a system comment, a mention-style run, an override table and endpoints.
It is now an ordinary issue_wakeup row with system_rule set, sharing the
condition, receipts, runaway protection, timeline entries and run model of
people's rules.
- One children_done evaluator: a stage wakes the assignee when it closes
while a later stage waits; the wrap-up waits for every sub-issue,
unstaged ones included. People's sub-issue conditions read the same set.
- issue_child_event records closing, reopening, re-parenting and restaging
for every writer; the request processes it right after commit and the
sweep retries. User sub-issue rules become immediate too.
- The target is resolved when it fires: an agent run, a squad leader run,
an inbox notification for a member, or only the timeline entry.
- A parked (backlog) parent catches up when it leaves backlog; a waiting
run of the same agent is joined instead of queuing a second one; the
hourly limit pauses a runaway rule. No system comment is posted.
- The run gets the issue's instruction, else the workspace's, else the
built-in one, plus each stage's counts and the next stage.
- Workspace defaults move to GET/PUT /api/system-wakeups (owners and
admins) and apply to every rule nobody customized; turning a rule on
records what already holds so old facts do not fire.
- Existing open parents get their rule from a one-time backfill.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(views): workspace wakeup settings and sub-issue notifications
- Settings → Wakeups holds the sub-issue rule's workspace default and
default instruction; the toggle leaves Issue statuses.
- The issue's system rule shows who it reaches (a member is notified),
a platform pause, and the instruction it will use.
- Timeline entries say whether the assignee was woken, notified or joined
a waiting run. The folding of the old system comment is gone.
- Inbox renders the new children_done notification on web, desktop and
mobile.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(server): stop wakeups from repeating their agent's runs
Runs are serialized per issue and agent, so a wakeup never ran beside
another run of its agent, but it queued behind one and repeated it. Each
rule now checks two things before it starts a run:
- Acknowledged: every input came from the agent itself. Its own comments
and issue changes never wake it, and a condition its own unfinished run
on the issue satisfied does not wake it when the platform or the agent
set the rule up. This fixes #8849: a coordinator closing its own stage is
not woken again while that run is going. A person's condition rule still
runs afterwards, since the running agent lacks its instruction.
- Merged: when a run of the agent is already waiting to start on the issue
(assignment, comment, another rule), the rule's instruction and facts
join that run (context.wakeup_joined, sent to the daemon as
wakeup_joined and rendered for every prompt kind) instead of queuing
another. Turning the rule off or replacing it withdraws what it joined.
Sub-issue changes now record the run that made them, and hints and
condition.met inputs carry those runs so a satisfied condition knows
whether the agent caused it. A rule that names the agent itself as the
actor keeps waking it on its own changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(views): show merged and acknowledged wakeups, hint assignee comment rules
- Timeline entries say when a wakeup joined the agent's waiting run or was
the agent's own doing, for people's rules and the sub-issue rule.
- The create form tells a member that a reply or comment rule for the
issue's agent assignee joins the run a comment already starts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(server): hand waiting wakeups to a run when it is claimed
A rule that fired while its agent had a run waiting used to write its
instruction into that run right away and consume its inputs. That let a
member's rule run under another member's identity, kept deleted rules'
instructions in the run, lost the inputs when the run was cancelled or
claimed by an older daemon, and skipped the fire cap and loop check.
Now the rule only waits for a run that runs as the same person its own
run would, keeping its inputs. When a daemon advertising
joined-wakeups-v1 claims that run, JoinWaitingWakeups rechecks each rule
(on, same agent and person, creator still allowed, not the agent's own
input, no loop), consumes its inputs, counts the fire toward max_fires,
adds its chain and records the merge. A re-claim drops entries of rules
turned off or changed since. Anything else leaves the rule its inputs to
start its own run.
A child_done rule created by the backfill or by processing a change now
treats sub-issues with unprocessed close events as still open, so the
backfill no longer swallows a close waiting for the sweep.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(server): consume joined wakeup inputs only once the run starts
A claim used to consume the inputs of the rules that joined the run. If
that claim did not go through, the run went back to the queue carrying
inputs the rules no longer had: turning the sub-issue rule off on the
issue or for the workspace (which only changes enabled) did not remove
them from the next claim, and cancelling the run lost a once rule that
had already ended.
A claim now reserves the inputs for the run (receipt task_id, still
unprocessed). The rule's next dispatch settles them: a run that started
consumes them as a merged firing (once ends, max_fires counts and can
pause, timeline entry); a run that ended without starting gives them
back; one that has not started keeps them. Turning a rule off or
changing it discards its pending inputs, reserved ones included, and a
later claim of the same run rechecks every entry and drops the ones
without reserved inputs or no longer allowed to reach the run.
Co-Authored-By: Claude Opus 5.5 <[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]>
1209 lines
40 KiB
Go
1209 lines
40 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: workspace_delete.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const deleteTaskBatch = `-- name: DeleteTaskBatch :exec
|
|
WITH
|
|
batch AS MATERIALIZED (
|
|
SELECT id FROM unnest($1::uuid[]) AS t(id)
|
|
),
|
|
deleted_task_usage AS (
|
|
DELETE FROM task_usage WHERE task_id IN (SELECT id FROM batch)
|
|
),
|
|
deleted_task_messages AS (
|
|
DELETE FROM task_message WHERE task_id IN (SELECT id FROM batch)
|
|
),
|
|
deleted_task_tokens AS (
|
|
DELETE FROM task_token WHERE task_id IN (SELECT id FROM batch)
|
|
),
|
|
deleted_task_supplements AS (
|
|
DELETE FROM task_supplement WHERE task_id IN (SELECT id FROM batch)
|
|
),
|
|
deleted_task_supplement_capabilities AS (
|
|
DELETE FROM task_supplement_capability WHERE task_id IN (SELECT id FROM batch)
|
|
),
|
|
deleted_channel_outbound_cards AS (
|
|
DELETE FROM channel_outbound_card_message WHERE task_id IN (SELECT id FROM batch)
|
|
),
|
|
deleted_lark_outbound_cards AS (
|
|
DELETE FROM lark_outbound_card_message WHERE task_id IN (SELECT id FROM batch)
|
|
),
|
|
deleted_channel_task_deliveries AS (
|
|
DELETE FROM channel_task_delivery WHERE task_id IN (SELECT id FROM batch)
|
|
),
|
|
deleted_draft_restores AS (
|
|
DELETE FROM chat_draft_restore WHERE task_id IN (SELECT id FROM batch)
|
|
)
|
|
DELETE FROM agent_task_queue WHERE id IN (SELECT id FROM batch)
|
|
`
|
|
|
|
// Deletes one bounded batch of tasks together with everything that hangs off
|
|
// them, every arm keyed by task_id against an existing index, then the task rows
|
|
// by primary key. The legacy FK cascades stay a safety net only: this statement
|
|
// is what actually removes the rows, so teardown keeps working when they go.
|
|
func (q *Queries) DeleteTaskBatch(ctx context.Context, taskIds []pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteTaskBatch, taskIds)
|
|
return err
|
|
}
|
|
|
|
const deleteTaskTokensByAgent = `-- name: DeleteTaskTokensByAgent :exec
|
|
DELETE FROM task_token WHERE agent_id = $1
|
|
`
|
|
|
|
// The third explicit task_token path. workspace_id (in DeleteWorkspaceLeafData)
|
|
// and task_id (in DeleteTaskBatch) do not cover a token whose own workspace_id
|
|
// points at a neighbour while its agent lives here, and teardown must not fall
|
|
// back on the agent_id cascade for it. Uses idx_task_token_agent_id
|
|
// (migration 275); one agent key per call for the same planner reason as
|
|
// ListTaskIDsByAgentBatch.
|
|
func (q *Queries) DeleteTaskTokensByAgent(ctx context.Context, agentID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteTaskTokensByAgent, agentID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspaceAdministration = `-- name: DeleteWorkspaceAdministration :exec
|
|
WITH
|
|
deleted_members AS (
|
|
DELETE FROM member WHERE member.workspace_id = $1
|
|
),
|
|
deleted_notification_preferences AS (
|
|
DELETE FROM notification_preference
|
|
WHERE notification_preference.workspace_id = $1
|
|
),
|
|
deleted_pins AS (
|
|
DELETE FROM pinned_item WHERE pinned_item.workspace_id = $1
|
|
),
|
|
deleted_daemon_tokens AS (
|
|
DELETE FROM daemon_token WHERE daemon_token.workspace_id = $1
|
|
),
|
|
detached_feedback AS (
|
|
UPDATE feedback
|
|
SET workspace_id = NULL
|
|
WHERE feedback.workspace_id = $1
|
|
),
|
|
detached_client_usage AS (
|
|
UPDATE client_usage_daily
|
|
SET workspace_id = NULL
|
|
WHERE client_usage_daily.workspace_id = $1
|
|
),
|
|
deleted_share_links AS (
|
|
DELETE FROM workspace_share_link
|
|
WHERE workspace_share_link.workspace_id = $1
|
|
)
|
|
DELETE FROM workspace_invitation
|
|
WHERE workspace_invitation.workspace_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteWorkspaceAdministration(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspaceAdministration, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspaceAgents = `-- name: DeleteWorkspaceAgents :exec
|
|
DELETE FROM agent WHERE agent.workspace_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteWorkspaceAgents(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspaceAgents, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspaceAutopilotChildren = `-- name: DeleteWorkspaceAutopilotChildren :exec
|
|
WITH
|
|
deleted_triggers AS (
|
|
DELETE FROM autopilot_trigger
|
|
WHERE autopilot_id IN (
|
|
SELECT id FROM autopilot WHERE autopilot.workspace_id = $1
|
|
)
|
|
)
|
|
DELETE FROM autopilot_rule_version
|
|
WHERE autopilot_rule_version.workspace_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteWorkspaceAutopilotChildren(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspaceAutopilotChildren, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspaceAutopilotQuotaPeriods = `-- name: DeleteWorkspaceAutopilotQuotaPeriods :exec
|
|
DELETE FROM autopilot_quota_period
|
|
WHERE autopilot_quota_period.workspace_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteWorkspaceAutopilotQuotaPeriods(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspaceAutopilotQuotaPeriods, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspaceAutopilotQuotaReservations = `-- name: DeleteWorkspaceAutopilotQuotaReservations :exec
|
|
DELETE FROM autopilot_quota_reservation
|
|
WHERE autopilot_quota_reservation.workspace_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteWorkspaceAutopilotQuotaReservations(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspaceAutopilotQuotaReservations, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspaceAutopilotRuns = `-- name: DeleteWorkspaceAutopilotRuns :exec
|
|
DELETE FROM autopilot_run
|
|
WHERE autopilot_id IN (
|
|
SELECT id FROM autopilot WHERE autopilot.workspace_id = $1
|
|
)
|
|
`
|
|
|
|
func (q *Queries) DeleteWorkspaceAutopilotRuns(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspaceAutopilotRuns, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspaceAutopilots = `-- name: DeleteWorkspaceAutopilots :exec
|
|
DELETE FROM autopilot WHERE autopilot.workspace_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteWorkspaceAutopilots(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspaceAutopilots, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspaceChatMessages = `-- name: DeleteWorkspaceChatMessages :exec
|
|
DELETE FROM chat_message
|
|
WHERE chat_session_id IN (
|
|
SELECT id FROM chat_session WHERE chat_session.workspace_id = $1
|
|
)
|
|
`
|
|
|
|
func (q *Queries) DeleteWorkspaceChatMessages(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspaceChatMessages, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspaceComments = `-- name: DeleteWorkspaceComments :exec
|
|
DELETE FROM comment WHERE comment.workspace_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteWorkspaceComments(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspaceComments, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspaceCommunicationRoots = `-- name: DeleteWorkspaceCommunicationRoots :exec
|
|
WITH
|
|
deleted_sessions AS (
|
|
DELETE FROM chat_session WHERE chat_session.workspace_id = $1
|
|
),
|
|
deleted_dingtalk_group_presence AS (
|
|
DELETE FROM dingtalk_group_presence WHERE workspace_id = $1
|
|
),
|
|
deleted_dingtalk_bot_identity AS (
|
|
DELETE FROM dingtalk_bot_identity WHERE workspace_id = $1
|
|
),
|
|
deleted_channel_installations AS (
|
|
DELETE FROM channel_installation
|
|
WHERE channel_installation.workspace_id = $1
|
|
)
|
|
DELETE FROM lark_installation WHERE lark_installation.workspace_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteWorkspaceCommunicationRoots(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspaceCommunicationRoots, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspaceConnections = `-- name: DeleteWorkspaceConnections :exec
|
|
WITH deleted_github_installations AS (
|
|
DELETE FROM github_installation
|
|
WHERE github_installation.workspace_id = $1
|
|
)
|
|
DELETE FROM vcs_connection WHERE vcs_connection.workspace_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteWorkspaceConnections(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspaceConnections, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspaceIssueRoots = `-- name: DeleteWorkspaceIssueRoots :exec
|
|
WITH deleted_wakeup_receipts AS (
|
|
DELETE FROM issue_wakeup_receipt WHERE wakeup_id IN (SELECT id FROM issue_wakeup WHERE workspace_id=$1)
|
|
), deleted_wakeups AS (
|
|
DELETE FROM issue_wakeup WHERE workspace_id=$1
|
|
),
|
|
deleted_child_events AS (
|
|
DELETE FROM issue_child_event WHERE workspace_id=$1
|
|
),
|
|
deleted_issues AS (
|
|
DELETE FROM issue WHERE issue.workspace_id = $1
|
|
),
|
|
deleted_labels AS (
|
|
DELETE FROM issue_label WHERE issue_label.workspace_id = $1
|
|
),
|
|
deleted_properties AS (
|
|
DELETE FROM issue_property WHERE issue_property.workspace_id = $1
|
|
),
|
|
deleted_issue_views AS (
|
|
DELETE FROM issue_view WHERE issue_view.workspace_id = $1
|
|
),
|
|
deleted_issue_view_preferences AS (
|
|
DELETE FROM issue_view_preference
|
|
WHERE issue_view_preference.workspace_id = $1
|
|
)
|
|
DELETE FROM quick_action WHERE quick_action.workspace_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteWorkspaceIssueRoots(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspaceIssueRoots, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspaceLeafData = `-- name: DeleteWorkspaceLeafData :exec
|
|
WITH
|
|
ws_agents AS MATERIALIZED (
|
|
SELECT id FROM agent WHERE workspace_id = $1
|
|
),
|
|
ws_issues AS MATERIALIZED (
|
|
SELECT id FROM issue WHERE workspace_id = $1
|
|
),
|
|
ws_labels AS MATERIALIZED (
|
|
SELECT id FROM issue_label WHERE workspace_id = $1
|
|
),
|
|
ws_skills AS MATERIALIZED (
|
|
SELECT id FROM skill WHERE workspace_id = $1
|
|
),
|
|
ws_squads AS MATERIALIZED (
|
|
SELECT id FROM squad WHERE workspace_id = $1
|
|
),
|
|
ws_sessions AS MATERIALIZED (
|
|
SELECT id FROM chat_session WHERE workspace_id = $1
|
|
),
|
|
ws_autopilots AS MATERIALIZED (
|
|
SELECT id FROM autopilot WHERE workspace_id = $1
|
|
),
|
|
ws_github_prs AS MATERIALIZED (
|
|
SELECT id FROM github_pull_request WHERE workspace_id = $1
|
|
),
|
|
ws_vcs_prs AS MATERIALIZED (
|
|
SELECT id FROM vcs_pull_request WHERE workspace_id = $1
|
|
),
|
|
ws_vcs_connections AS MATERIALIZED (
|
|
SELECT id FROM vcs_connection WHERE workspace_id = $1
|
|
),
|
|
ws_channel_installations AS MATERIALIZED (
|
|
SELECT id FROM channel_installation WHERE workspace_id = $1
|
|
),
|
|
ws_lark_installations AS MATERIALIZED (
|
|
SELECT id FROM lark_installation WHERE workspace_id = $1
|
|
),
|
|
deleted_task_tokens AS (
|
|
DELETE FROM task_token
|
|
WHERE workspace_id = $1
|
|
),
|
|
deleted_orphan_task_supplements AS (
|
|
DELETE FROM task_supplement WHERE workspace_id = $1
|
|
),
|
|
deleted_orphan_task_supplement_capabilities AS (
|
|
DELETE FROM task_supplement_capability WHERE workspace_id = $1
|
|
),
|
|
deleted_hourly_dirty AS (
|
|
DELETE FROM task_usage_hourly_dirty WHERE workspace_id = $1
|
|
),
|
|
deleted_hourly AS (
|
|
DELETE FROM task_usage_hourly WHERE workspace_id = $1
|
|
),
|
|
deleted_attachments AS (
|
|
DELETE FROM attachment WHERE workspace_id = $1
|
|
),
|
|
deleted_channel_outbound_cards AS (
|
|
DELETE FROM channel_outbound_card_message
|
|
WHERE chat_session_id IN (SELECT id FROM ws_sessions)
|
|
),
|
|
deleted_lark_outbound_cards AS (
|
|
DELETE FROM lark_outbound_card_message
|
|
WHERE chat_session_id IN (SELECT id FROM ws_sessions)
|
|
),
|
|
deleted_draft_restores AS (
|
|
DELETE FROM chat_draft_restore
|
|
WHERE chat_session_id IN (SELECT id FROM ws_sessions)
|
|
),
|
|
deleted_agent_builder_drafts AS (
|
|
DELETE FROM agent_builder_draft WHERE workspace_id = $1
|
|
),
|
|
deleted_comment_reactions AS (
|
|
DELETE FROM comment_reaction WHERE workspace_id = $1
|
|
),
|
|
deleted_issue_reactions AS (
|
|
DELETE FROM issue_reaction WHERE workspace_id = $1
|
|
),
|
|
deleted_activity AS (
|
|
DELETE FROM activity_log WHERE workspace_id = $1
|
|
),
|
|
deleted_inbox AS (
|
|
DELETE FROM inbox_item WHERE workspace_id = $1
|
|
),
|
|
deleted_issue_dependencies AS (
|
|
DELETE FROM issue_dependency
|
|
WHERE issue_id IN (SELECT id FROM ws_issues)
|
|
OR depends_on_issue_id IN (SELECT id FROM ws_issues)
|
|
),
|
|
deleted_issue_subscribers AS (
|
|
DELETE FROM issue_subscriber
|
|
WHERE issue_id IN (SELECT id FROM ws_issues)
|
|
),
|
|
deleted_issue_labels AS (
|
|
DELETE FROM issue_to_label
|
|
WHERE issue_id IN (SELECT id FROM ws_issues)
|
|
OR label_id IN (SELECT id FROM ws_labels)
|
|
),
|
|
deleted_agent_labels AS (
|
|
DELETE FROM agent_to_label
|
|
WHERE agent_id IN (SELECT id FROM ws_agents)
|
|
OR label_id IN (SELECT id FROM ws_labels)
|
|
),
|
|
deleted_skill_labels AS (
|
|
DELETE FROM skill_to_label
|
|
WHERE skill_id IN (SELECT id FROM ws_skills)
|
|
OR label_id IN (SELECT id FROM ws_labels)
|
|
),
|
|
deleted_issue_github_links AS (
|
|
DELETE FROM issue_pull_request
|
|
WHERE issue_id IN (SELECT id FROM ws_issues)
|
|
OR pull_request_id IN (SELECT id FROM ws_github_prs)
|
|
),
|
|
deleted_issue_vcs_links AS (
|
|
DELETE FROM issue_vcs_pull_request
|
|
WHERE issue_id IN (SELECT id FROM ws_issues)
|
|
OR pull_request_id IN (SELECT id FROM ws_vcs_prs)
|
|
),
|
|
deleted_issue_pr_automation AS (
|
|
DELETE FROM issue_pr_automation WHERE workspace_id = $1
|
|
),
|
|
deleted_issue_pr_exclusions AS (
|
|
DELETE FROM issue_pull_request_exclusion WHERE workspace_id = $1
|
|
),
|
|
deleted_agent_invocation_targets AS (
|
|
DELETE FROM agent_invocation_target
|
|
WHERE agent_id IN (SELECT id FROM ws_agents)
|
|
),
|
|
deleted_agent_skills AS (
|
|
DELETE FROM agent_skill
|
|
WHERE agent_id IN (SELECT id FROM ws_agents)
|
|
OR skill_id IN (SELECT id FROM ws_skills)
|
|
),
|
|
deleted_skill_files AS (
|
|
DELETE FROM skill_file
|
|
WHERE skill_id IN (SELECT id FROM ws_skills)
|
|
),
|
|
deleted_daemon_connections AS (
|
|
DELETE FROM daemon_connection
|
|
WHERE agent_id IN (SELECT id FROM ws_agents)
|
|
),
|
|
deleted_squad_members AS (
|
|
DELETE FROM squad_member
|
|
WHERE squad_id IN (SELECT id FROM ws_squads)
|
|
),
|
|
deleted_project_resources AS (
|
|
DELETE FROM project_resource WHERE workspace_id = $1
|
|
),
|
|
deleted_autopilot_collaborators AS (
|
|
DELETE FROM autopilot_collaborator
|
|
WHERE autopilot_id IN (SELECT id FROM ws_autopilots)
|
|
),
|
|
deleted_autopilot_subscribers AS (
|
|
DELETE FROM autopilot_subscriber
|
|
WHERE autopilot_id IN (SELECT id FROM ws_autopilots)
|
|
),
|
|
deleted_webhook_deliveries AS (
|
|
DELETE FROM webhook_delivery WHERE workspace_id = $1
|
|
),
|
|
deleted_github_check_runs AS (
|
|
DELETE FROM github_pull_request_check_run
|
|
WHERE pr_id IN (SELECT id FROM ws_github_prs)
|
|
),
|
|
deleted_github_check_suites AS (
|
|
DELETE FROM github_pull_request_check_suite
|
|
WHERE pr_id IN (SELECT id FROM ws_github_prs)
|
|
),
|
|
deleted_pending_github_suites AS (
|
|
DELETE FROM github_pending_check_suite WHERE workspace_id = $1
|
|
),
|
|
deleted_channel_task_deliveries AS (
|
|
DELETE FROM channel_task_delivery
|
|
WHERE installation_id IN (SELECT id FROM ws_channel_installations)
|
|
),
|
|
deleted_channel_outbound_messages AS (
|
|
DELETE FROM channel_outbound_message
|
|
WHERE installation_id IN (SELECT id FROM ws_channel_installations)
|
|
),
|
|
deleted_channel_reply_deliveries AS (
|
|
DELETE FROM channel_reply_delivery
|
|
WHERE installation_id IN (SELECT id FROM ws_channel_installations)
|
|
),
|
|
deleted_channel_chat_contexts AS (
|
|
DELETE FROM channel_chat_context_generation
|
|
WHERE chat_session_id IN (SELECT id FROM ws_sessions)
|
|
),
|
|
deleted_vcs_commit_statuses AS (
|
|
DELETE FROM vcs_commit_status
|
|
WHERE connection_id IN (SELECT id FROM ws_vcs_connections)
|
|
),
|
|
deleted_channel_chat_bindings AS (
|
|
DELETE FROM channel_chat_session_binding
|
|
WHERE installation_id IN (SELECT id FROM ws_channel_installations)
|
|
OR chat_session_id IN (SELECT id FROM ws_sessions)
|
|
),
|
|
deleted_dingtalk_group_routes AS (
|
|
DELETE FROM dingtalk_group_route WHERE workspace_id = $1
|
|
),
|
|
deleted_channel_inbound_dedup AS (
|
|
DELETE FROM channel_inbound_message_dedup
|
|
WHERE installation_id IN (SELECT id FROM ws_channel_installations)
|
|
),
|
|
deleted_channel_inbound_audit AS (
|
|
DELETE FROM channel_inbound_audit
|
|
WHERE installation_id IN (SELECT id FROM ws_channel_installations)
|
|
),
|
|
deleted_channel_user_bindings AS (
|
|
DELETE FROM channel_user_binding WHERE workspace_id = $1
|
|
),
|
|
deleted_channel_binding_tokens AS (
|
|
DELETE FROM channel_binding_token WHERE workspace_id = $1
|
|
),
|
|
deleted_lark_chat_bindings AS (
|
|
DELETE FROM lark_chat_session_binding
|
|
WHERE installation_id IN (SELECT id FROM ws_lark_installations)
|
|
OR chat_session_id IN (SELECT id FROM ws_sessions)
|
|
),
|
|
deleted_lark_inbound_dedup AS (
|
|
DELETE FROM lark_inbound_message_dedup
|
|
WHERE installation_id IN (SELECT id FROM ws_lark_installations)
|
|
),
|
|
deleted_lark_inbound_audit AS (
|
|
DELETE FROM lark_inbound_audit
|
|
WHERE installation_id IN (SELECT id FROM ws_lark_installations)
|
|
),
|
|
deleted_lark_user_bindings AS (
|
|
DELETE FROM lark_user_binding WHERE workspace_id = $1
|
|
),
|
|
deleted_lark_binding_tokens AS (
|
|
DELETE FROM lark_binding_token WHERE workspace_id = $1
|
|
)
|
|
UPDATE channel_media_pending_object
|
|
SET state = CASE
|
|
WHEN state = 'tombstoned' THEN 'tombstoned'
|
|
ELSE 'deleting'
|
|
END,
|
|
lease_token = NULL,
|
|
lease_expires_at = NULL,
|
|
next_attempt_at = now(),
|
|
last_error = NULL
|
|
WHERE channel_media_pending_object.workspace_id = $1
|
|
`
|
|
|
|
// Everything task-keyed moved to DeleteTaskBatch, which runs in bounded batches
|
|
// before this step; what is left is keyed by the workspace or by one of the
|
|
// workspace-scoped id sets below.
|
|
// One of three explicit task_token paths. This is the workspace-keyed one;
|
|
// DeleteTaskBatch covers task_id and DeleteTaskTokensByAgent covers agent_id, so
|
|
// a token whose workspace_id points at a neighbour while its task or agent lives
|
|
// here is still removed by this teardown rather than by the FK cascade. The
|
|
// former single statement combined all three with OR, which cost a full scan of
|
|
// task_token (MUL-5999); split, each path is an index scan.
|
|
// Same no-FK chore as chat_draft_restore above. Matched on workspace_id rather
|
|
// than the session set because that column exists precisely so this statement
|
|
// does not have to join through chat_session, which it deletes in this same CTE.
|
|
// Keep the two-system cleanup ledger until object storage has been settled.
|
|
// Moving every row out of pending also prevents a concurrent media bind from
|
|
// attaching an object after the workspace teardown commits. The reconciler
|
|
// performs the idempotent object delete and clears the row afterwards.
|
|
func (q *Queries) DeleteWorkspaceLeafData(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspaceLeafData, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspacePluginData = `-- name: DeleteWorkspacePluginData :exec
|
|
WITH installations AS MATERIALIZED (
|
|
SELECT plugin_installation.id
|
|
FROM plugin_installation
|
|
WHERE plugin_installation.workspace_id = $1
|
|
),
|
|
deleted_storage AS (
|
|
DELETE FROM plugin_storage
|
|
WHERE installation_id IN (SELECT id FROM installations)
|
|
),
|
|
deleted_secrets AS (
|
|
DELETE FROM plugin_secret
|
|
WHERE installation_id IN (SELECT id FROM installations)
|
|
),
|
|
deleted_hook_schedules AS (
|
|
DELETE FROM plugin_hook_schedule
|
|
WHERE installation_id IN (SELECT id FROM installations)
|
|
),
|
|
deleted_invocations AS (
|
|
DELETE FROM plugin_invocation
|
|
WHERE workspace_id = $1
|
|
),
|
|
versions AS MATERIALIZED (
|
|
SELECT plugin_package_version.id
|
|
FROM plugin_package_version
|
|
WHERE plugin_package_version.workspace_id = $1
|
|
),
|
|
deleted_package_files AS (
|
|
DELETE FROM plugin_package_file
|
|
WHERE version_id IN (SELECT id FROM versions)
|
|
),
|
|
deleted_package_versions AS (
|
|
DELETE FROM plugin_package_version
|
|
WHERE workspace_id = $1
|
|
),
|
|
deleted_packages AS (
|
|
DELETE FROM plugin_package
|
|
WHERE workspace_id = $1
|
|
)
|
|
DELETE FROM plugin_installation WHERE id IN (SELECT id FROM installations)
|
|
`
|
|
|
|
// Plugin relationships have no foreign keys or cascades. Storage and secrets
|
|
// hang off the installation, so both leaf tables are cleared through the
|
|
// workspace's installation ids before the installations themselves.
|
|
// Hook call records are workspace-scoped in their own right, so this deletes by
|
|
// workspace rather than through the installation ids: a row whose installation
|
|
// was already uninstalled would otherwise survive the workspace it described.
|
|
// Published artifacts are workspace-scoped too, and independent of whether
|
|
// anything installed them. Deleting the workspace without these would leave the
|
|
// stored bundles as the largest orphan the plugin surface can produce.
|
|
func (q *Queries) DeleteWorkspacePluginData(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspacePluginData, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspacePullRequests = `-- name: DeleteWorkspacePullRequests :exec
|
|
WITH deleted_github_prs AS (
|
|
DELETE FROM github_pull_request
|
|
WHERE github_pull_request.workspace_id = $1
|
|
)
|
|
DELETE FROM vcs_pull_request WHERE vcs_pull_request.workspace_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteWorkspacePullRequests(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspacePullRequests, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspaceRuntimesAndProjects = `-- name: DeleteWorkspaceRuntimesAndProjects :exec
|
|
WITH
|
|
deleted_runtimes AS (
|
|
DELETE FROM agent_runtime WHERE agent_runtime.workspace_id = $1
|
|
),
|
|
deleted_profiles AS (
|
|
DELETE FROM runtime_profile WHERE runtime_profile.workspace_id = $1
|
|
)
|
|
DELETE FROM project WHERE project.workspace_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteWorkspaceRuntimesAndProjects(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspaceRuntimesAndProjects, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspaceSquadsAndSkills = `-- name: DeleteWorkspaceSquadsAndSkills :exec
|
|
WITH deleted_squads AS (
|
|
DELETE FROM squad WHERE squad.workspace_id = $1
|
|
)
|
|
DELETE FROM skill WHERE skill.workspace_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteWorkspaceSquadsAndSkills(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteWorkspaceSquadsAndSkills, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const detachTaskBatchReferences = `-- name: DetachTaskBatchReferences :exec
|
|
WITH detached_runs AS (
|
|
UPDATE autopilot_run
|
|
SET task_id = NULL
|
|
WHERE task_id = ANY($1::uuid[])
|
|
)
|
|
UPDATE agent_task_queue
|
|
SET parent_task_id = NULL
|
|
WHERE parent_task_id = ANY($1::uuid[])
|
|
`
|
|
|
|
// Break inbound references to a task batch before deleting it, in the
|
|
// application layer rather than through the FKs' ON DELETE SET NULL. Both
|
|
// referencing columns are indexed (idx_agent_task_queue_parent from migration
|
|
// 055, idx_autopilot_run_task_id from migration 277) so this stays a per-batch
|
|
// index scan instead of the per-row full scan the FK action would do.
|
|
//
|
|
// Kept separate from DeleteTaskBatch: a task can be both a member of the batch
|
|
// and the parent of another member, and updating plus deleting the same row
|
|
// inside one statement is not well defined.
|
|
func (q *Queries) DetachTaskBatchReferences(ctx context.Context, taskIds []pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, detachTaskBatchReferences, taskIds)
|
|
return err
|
|
}
|
|
|
|
const listTaskIDsByAgentFirstPage = `-- name: ListTaskIDsByAgentFirstPage :many
|
|
SELECT id FROM agent_task_queue
|
|
WHERE agent_id = $1
|
|
ORDER BY id
|
|
LIMIT $2
|
|
FOR UPDATE
|
|
`
|
|
|
|
type ListTaskIDsByAgentFirstPageParams struct {
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
Limit int32 `json:"limit"`
|
|
}
|
|
|
|
// First page of the same walk; see ListWorkspaceAgentIDFirstPage for why this is
|
|
// a separate query rather than a zero-uuid cursor, and ListTaskIDsByAgentPage for
|
|
// the single-key / keyset / FOR UPDATE rationale.
|
|
func (q *Queries) ListTaskIDsByAgentFirstPage(ctx context.Context, arg ListTaskIDsByAgentFirstPageParams) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, listTaskIDsByAgentFirstPage, arg.AgentID, arg.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []pgtype.UUID{}
|
|
for rows.Next() {
|
|
var id pgtype.UUID
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listTaskIDsByAgentPage = `-- name: ListTaskIDsByAgentPage :many
|
|
SELECT id FROM agent_task_queue
|
|
WHERE agent_id = $1 AND id > $2
|
|
ORDER BY id
|
|
LIMIT $3
|
|
FOR UPDATE
|
|
`
|
|
|
|
type ListTaskIDsByAgentPageParams struct {
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
ID pgtype.UUID `json:"id"`
|
|
Limit int32 `json:"limit"`
|
|
}
|
|
|
|
// One owner key per call, one keyset-paged chunk per call.
|
|
//
|
|
// Single-key equality is deliberate. All three ownership paths must stay (nothing
|
|
// enforces that a task's agent/runtime/issue share a workspace), but expressing
|
|
// them as `OR agent_id IN (subquery) OR …` — or as a UNION of joins, or as
|
|
// `= ANY($1::uuid[])` over a whole workspace's agents — makes the planner
|
|
// estimate a proportional share of the global table and fall back to a Seq Scan
|
|
// on agent_task_queue. A single-key equality is the only form whose row estimate
|
|
// stays small enough to be index-driven regardless of how the workspace's tasks
|
|
// are distributed (MUL-5999).
|
|
//
|
|
// `id > $2 ORDER BY id LIMIT $3` against idx_agent_task_queue_agent_id_keyset
|
|
// (migration 278) bounds the SCAN, not just the result: each page is an index
|
|
// range scan that starts where the previous one stopped. Bounding only the result
|
|
// — `ORDER BY id LIMIT n` against the (agent_id, status) index — puts a Sort over
|
|
// the agent's entire task set in front of every page, so a busy agent costs
|
|
// roughly O(N² / page) for the sweep.
|
|
//
|
|
// FOR UPDATE is load-bearing for correctness, not throughput. It locks the rows
|
|
// this page is about to delete, and under READ COMMITTED PostgreSQL re-checks the
|
|
// WHERE clause after acquiring each lock: a task a concurrent committed UPDATE
|
|
// has already moved to another owner no longer matches and is skipped, and one we
|
|
// did lock cannot be moved away before we delete it.
|
|
func (q *Queries) ListTaskIDsByAgentPage(ctx context.Context, arg ListTaskIDsByAgentPageParams) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, listTaskIDsByAgentPage, arg.AgentID, arg.ID, arg.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []pgtype.UUID{}
|
|
for rows.Next() {
|
|
var id pgtype.UUID
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listTaskIDsByIssueFirstPage = `-- name: ListTaskIDsByIssueFirstPage :many
|
|
SELECT id FROM agent_task_queue
|
|
WHERE issue_id = $1
|
|
ORDER BY id
|
|
LIMIT $2
|
|
FOR UPDATE
|
|
`
|
|
|
|
type ListTaskIDsByIssueFirstPageParams struct {
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
Limit int32 `json:"limit"`
|
|
}
|
|
|
|
// First page of the same walk; see ListWorkspaceAgentIDFirstPage for why this is
|
|
// a separate query rather than a zero-uuid cursor, and ListTaskIDsByAgentPage for
|
|
// the single-key / keyset / FOR UPDATE rationale.
|
|
func (q *Queries) ListTaskIDsByIssueFirstPage(ctx context.Context, arg ListTaskIDsByIssueFirstPageParams) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, listTaskIDsByIssueFirstPage, arg.IssueID, arg.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []pgtype.UUID{}
|
|
for rows.Next() {
|
|
var id pgtype.UUID
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listTaskIDsByIssuePage = `-- name: ListTaskIDsByIssuePage :many
|
|
SELECT id FROM agent_task_queue
|
|
WHERE issue_id = $1 AND id > $2
|
|
ORDER BY id
|
|
LIMIT $3
|
|
FOR UPDATE
|
|
`
|
|
|
|
type ListTaskIDsByIssuePageParams struct {
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
ID pgtype.UUID `json:"id"`
|
|
Limit int32 `json:"limit"`
|
|
}
|
|
|
|
// Uses idx_agent_task_queue_issue_id_keyset (migration 279). See
|
|
// ListTaskIDsByAgentPage for the single-key / keyset / FOR UPDATE rationale.
|
|
func (q *Queries) ListTaskIDsByIssuePage(ctx context.Context, arg ListTaskIDsByIssuePageParams) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, listTaskIDsByIssuePage, arg.IssueID, arg.ID, arg.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []pgtype.UUID{}
|
|
for rows.Next() {
|
|
var id pgtype.UUID
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listTaskIDsByRuntimeFirstPage = `-- name: ListTaskIDsByRuntimeFirstPage :many
|
|
SELECT id FROM agent_task_queue
|
|
WHERE runtime_id = $1
|
|
ORDER BY id
|
|
LIMIT $2
|
|
FOR UPDATE
|
|
`
|
|
|
|
type ListTaskIDsByRuntimeFirstPageParams struct {
|
|
RuntimeID pgtype.UUID `json:"runtime_id"`
|
|
Limit int32 `json:"limit"`
|
|
}
|
|
|
|
// First page of the same walk; see ListWorkspaceAgentIDFirstPage for why this is
|
|
// a separate query rather than a zero-uuid cursor, and ListTaskIDsByAgentPage for
|
|
// the single-key / keyset / FOR UPDATE rationale.
|
|
func (q *Queries) ListTaskIDsByRuntimeFirstPage(ctx context.Context, arg ListTaskIDsByRuntimeFirstPageParams) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, listTaskIDsByRuntimeFirstPage, arg.RuntimeID, arg.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []pgtype.UUID{}
|
|
for rows.Next() {
|
|
var id pgtype.UUID
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listTaskIDsByRuntimePage = `-- name: ListTaskIDsByRuntimePage :many
|
|
SELECT id FROM agent_task_queue
|
|
WHERE runtime_id = $1 AND id > $2
|
|
ORDER BY id
|
|
LIMIT $3
|
|
FOR UPDATE
|
|
`
|
|
|
|
type ListTaskIDsByRuntimePageParams struct {
|
|
RuntimeID pgtype.UUID `json:"runtime_id"`
|
|
ID pgtype.UUID `json:"id"`
|
|
Limit int32 `json:"limit"`
|
|
}
|
|
|
|
// Uses idx_agent_task_queue_runtime_id (migration 273, (runtime_id, id)); before
|
|
// it every runtime_id index was partial, so this path had none at all. See
|
|
// ListTaskIDsByAgentPage for the single-key / keyset / FOR UPDATE rationale.
|
|
func (q *Queries) ListTaskIDsByRuntimePage(ctx context.Context, arg ListTaskIDsByRuntimePageParams) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, listTaskIDsByRuntimePage, arg.RuntimeID, arg.ID, arg.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []pgtype.UUID{}
|
|
for rows.Next() {
|
|
var id pgtype.UUID
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listWorkspaceAgentIDFirstPage = `-- name: ListWorkspaceAgentIDFirstPage :many
|
|
SELECT id FROM agent
|
|
WHERE agent.workspace_id = $1
|
|
ORDER BY id
|
|
LIMIT $2
|
|
`
|
|
|
|
type ListWorkspaceAgentIDFirstPageParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Limit int32 `json:"limit"`
|
|
}
|
|
|
|
// First page of the same walk. Split from the keyset query rather than seeded
|
|
// with the all-zero uuid, because that value is itself a valid uuid: a row whose
|
|
// id happened to be all zeros would be skipped forever by `id > $2`.
|
|
func (q *Queries) ListWorkspaceAgentIDFirstPage(ctx context.Context, arg ListWorkspaceAgentIDFirstPageParams) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, listWorkspaceAgentIDFirstPage, arg.WorkspaceID, arg.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []pgtype.UUID{}
|
|
for rows.Next() {
|
|
var id pgtype.UUID
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listWorkspaceAgentIDPage = `-- name: ListWorkspaceAgentIDPage :many
|
|
SELECT id FROM agent
|
|
WHERE agent.workspace_id = $1 AND id > $2
|
|
ORDER BY id
|
|
LIMIT $3
|
|
`
|
|
|
|
type ListWorkspaceAgentIDPageParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ID pgtype.UUID `json:"id"`
|
|
Limit int32 `json:"limit"`
|
|
}
|
|
|
|
// Owner enumeration in bounded, keyset-paged chunks. The rows are already locked
|
|
// by LockWorkspaceTaskOwnerAgents above; this only walks them.
|
|
//
|
|
// Keyset on id, not on a natural key: the cursor column must be immutable, and
|
|
// renaming an agent mid-teardown would otherwise let it sort behind the cursor
|
|
// and escape the sweep. Uses idx_agent_workspace_id_keyset (migration 281), which
|
|
// exists so this page does not sort the whole owner set.
|
|
func (q *Queries) ListWorkspaceAgentIDPage(ctx context.Context, arg ListWorkspaceAgentIDPageParams) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, listWorkspaceAgentIDPage, arg.WorkspaceID, arg.ID, arg.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []pgtype.UUID{}
|
|
for rows.Next() {
|
|
var id pgtype.UUID
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listWorkspaceIssueIDFirstPage = `-- name: ListWorkspaceIssueIDFirstPage :many
|
|
SELECT id FROM issue
|
|
WHERE issue.workspace_id = $1
|
|
ORDER BY id
|
|
LIMIT $2
|
|
`
|
|
|
|
type ListWorkspaceIssueIDFirstPageParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Limit int32 `json:"limit"`
|
|
}
|
|
|
|
// First page of the same walk. Split from the keyset query rather than seeded
|
|
// with the all-zero uuid, because that value is itself a valid uuid: a row whose
|
|
// id happened to be all zeros would be skipped forever by `id > $2`.
|
|
func (q *Queries) ListWorkspaceIssueIDFirstPage(ctx context.Context, arg ListWorkspaceIssueIDFirstPageParams) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, listWorkspaceIssueIDFirstPage, arg.WorkspaceID, arg.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []pgtype.UUID{}
|
|
for rows.Next() {
|
|
var id pgtype.UUID
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listWorkspaceIssueIDPage = `-- name: ListWorkspaceIssueIDPage :many
|
|
SELECT id FROM issue
|
|
WHERE issue.workspace_id = $1 AND id > $2
|
|
ORDER BY id
|
|
LIMIT $3
|
|
`
|
|
|
|
type ListWorkspaceIssueIDPageParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ID pgtype.UUID `json:"id"`
|
|
Limit int32 `json:"limit"`
|
|
}
|
|
|
|
// Uses idx_issue_workspace_id_keyset (migration 282).
|
|
func (q *Queries) ListWorkspaceIssueIDPage(ctx context.Context, arg ListWorkspaceIssueIDPageParams) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, listWorkspaceIssueIDPage, arg.WorkspaceID, arg.ID, arg.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []pgtype.UUID{}
|
|
for rows.Next() {
|
|
var id pgtype.UUID
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listWorkspaceRuntimeIDFirstPage = `-- name: ListWorkspaceRuntimeIDFirstPage :many
|
|
SELECT id FROM agent_runtime
|
|
WHERE agent_runtime.workspace_id = $1
|
|
ORDER BY id
|
|
LIMIT $2
|
|
`
|
|
|
|
type ListWorkspaceRuntimeIDFirstPageParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Limit int32 `json:"limit"`
|
|
}
|
|
|
|
// First page of the same walk. Split from the keyset query rather than seeded
|
|
// with the all-zero uuid, because that value is itself a valid uuid: a row whose
|
|
// id happened to be all zeros would be skipped forever by `id > $2`.
|
|
func (q *Queries) ListWorkspaceRuntimeIDFirstPage(ctx context.Context, arg ListWorkspaceRuntimeIDFirstPageParams) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, listWorkspaceRuntimeIDFirstPage, arg.WorkspaceID, arg.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []pgtype.UUID{}
|
|
for rows.Next() {
|
|
var id pgtype.UUID
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listWorkspaceRuntimeIDPage = `-- name: ListWorkspaceRuntimeIDPage :many
|
|
SELECT id FROM agent_runtime
|
|
WHERE agent_runtime.workspace_id = $1 AND id > $2
|
|
ORDER BY id
|
|
LIMIT $3
|
|
`
|
|
|
|
type ListWorkspaceRuntimeIDPageParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ID pgtype.UUID `json:"id"`
|
|
Limit int32 `json:"limit"`
|
|
}
|
|
|
|
// Uses idx_agent_runtime_workspace_id_keyset (migration 283).
|
|
func (q *Queries) ListWorkspaceRuntimeIDPage(ctx context.Context, arg ListWorkspaceRuntimeIDPageParams) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, listWorkspaceRuntimeIDPage, arg.WorkspaceID, arg.ID, arg.Limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []pgtype.UUID{}
|
|
for rows.Next() {
|
|
var id pgtype.UUID
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const lockTaskUsageRollupForWorkspaceDelete = `-- name: LockTaskUsageRollupForWorkspaceDelete :exec
|
|
SELECT pg_advisory_xact_lock(4246)
|
|
`
|
|
|
|
// Advisory lock 4246 is the hourly rollup family's key: the rollup function
|
|
// takes it per transaction (migration 272) and the backfill commands take it
|
|
// per session. An in-flight rollup finishes first, then no new rollup can
|
|
// write the workspace's aggregates until this delete commits. The caller
|
|
// bounds this wait with SET LOCAL lock_timeout — batch jobs hold 4246 for
|
|
// minutes, and an unbounded wait here hangs the delete request (MUL-5983).
|
|
func (q *Queries) LockTaskUsageRollupForWorkspaceDelete(ctx context.Context) error {
|
|
_, err := q.db.Exec(ctx, lockTaskUsageRollupForWorkspaceDelete)
|
|
return err
|
|
}
|
|
|
|
const lockWorkspaceTaskOwnerAgents = `-- name: LockWorkspaceTaskOwnerAgents :exec
|
|
SELECT 1 FROM agent WHERE agent.workspace_id = $1 FOR UPDATE
|
|
`
|
|
|
|
// The three LockWorkspaceTaskOwner* statements are a write fence, not a lookup —
|
|
// they deliberately return nothing, so a workspace with a huge owner set costs
|
|
// the API process no memory at all.
|
|
//
|
|
// agent_task_queue has no workspace_id, so a task becomes this workspace's
|
|
// problem through agent_id, issue_id or runtime_id — and inserting such a task
|
|
// requires FOR KEY SHARE on the referenced owner row, which conflicts with
|
|
// FOR UPDATE. Holding these locks for the rest of the teardown transaction
|
|
// therefore blocks any enqueue (or reassignment) that would add a task to this
|
|
// workspace after we have swept it, which row locks on the tasks alone cannot
|
|
// do. Same technique the handler already uses on the workspace row to keep
|
|
// CreateChatSession out of the delete window.
|
|
//
|
|
// This fence is defence in depth, not the only guarantee: deleteWorkspaceTasks
|
|
// re-verifies every owner after sweeping it and fails closed if tasks keep
|
|
// appearing, so teardown does not silently leak rows even if the fence is ever
|
|
// weakened (for example by removing the legacy FKs this one leans on).
|
|
func (q *Queries) LockWorkspaceTaskOwnerAgents(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, lockWorkspaceTaskOwnerAgents, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const lockWorkspaceTaskOwnerIssues = `-- name: LockWorkspaceTaskOwnerIssues :exec
|
|
SELECT 1 FROM issue WHERE issue.workspace_id = $1 FOR UPDATE
|
|
`
|
|
|
|
func (q *Queries) LockWorkspaceTaskOwnerIssues(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, lockWorkspaceTaskOwnerIssues, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const lockWorkspaceTaskOwnerRuntimes = `-- name: LockWorkspaceTaskOwnerRuntimes :exec
|
|
SELECT 1 FROM agent_runtime WHERE agent_runtime.workspace_id = $1 FOR UPDATE
|
|
`
|
|
|
|
func (q *Queries) LockWorkspaceTaskOwnerRuntimes(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, lockWorkspaceTaskOwnerRuntimes, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const prepareWorkspaceDeletionLinks = `-- name: PrepareWorkspaceDeletionLinks :exec
|
|
WITH
|
|
detached_comments AS (
|
|
UPDATE comment
|
|
SET parent_id = NULL
|
|
WHERE comment.workspace_id = $1
|
|
AND parent_id IS NOT NULL
|
|
),
|
|
detached_issues AS (
|
|
UPDATE issue
|
|
SET parent_issue_id = NULL
|
|
WHERE issue.workspace_id = $1
|
|
AND parent_issue_id IS NOT NULL
|
|
)
|
|
UPDATE webhook_delivery
|
|
SET replayed_from_delivery_id = NULL
|
|
WHERE webhook_delivery.workspace_id = $1
|
|
AND replayed_from_delivery_id IS NOT NULL
|
|
`
|
|
|
|
// Break self-references and the task/run cycle explicitly before deleting
|
|
// either side. This keeps their ordering in the application-owned graph
|
|
// instead of relying on ON DELETE SET NULL actions.
|
|
//
|
|
// The task half of this step now lives in DetachTaskBatchReferences, which runs
|
|
// once per bounded batch instead of once for a whole workspace's task set.
|
|
func (q *Queries) PrepareWorkspaceDeletionLinks(ctx context.Context, workspaceID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, prepareWorkspaceDeletionLinks, workspaceID)
|
|
return err
|
|
}
|
|
|
|
const setWorkspaceTeardownMode = `-- name: SetWorkspaceTeardownMode :exec
|
|
|
|
SELECT set_config('multica.workspace_teardown', 'on', true)
|
|
`
|
|
|
|
// Workspace deletion is application-owned. These statements form a fixed,
|
|
// bottom-up deletion plan; the legacy foreign keys remain only as a
|
|
// compatibility safety net during the expand phase.
|
|
// The setting is transaction-local. Migration 242 makes only the three DELETE
|
|
// dirty triggers skip their per-row work while this flag is on.
|
|
func (q *Queries) SetWorkspaceTeardownMode(ctx context.Context) error {
|
|
_, err := q.db.Exec(ctx, setWorkspaceTeardownMode)
|
|
return err
|
|
}
|