Files
multica/server/pkg/db/generated/workspace_delete.sql.go
Naiyuan Qing a5c1d44701 MUL-5642: fix(agents): stop the creation studio polling, and stop it losing work (#6307)
* feat(agents): make AI agent creation resumable (#6246)

Leaving the Agent Creation Studio destroyed the conversation. The unmount
cleanup called deleteChatSession, so a sidebar click, a tab close or a route
change deleted the builder session and every message in it — the bug external
users reported. Archiving instead (PR #6247) would have stopped the deletion
without giving anyone a way back in: builder sessions hang off a hidden
`kind = 'system'` carrier agent, which the `kind = 'user'` filter keeps out of
every chat list, so an archived one is unreachable rather than recoverable.

A creation conversation is now a durable object with its own address.

Server:
  - GET /api/agent-builder/sessions lists the caller's unfinished creations.
    Creator-scoped like every other chat read. It reports the CARRIER's
    runtime, not chat_session.runtime_id — the latter is the daemon's resume
    pointer and is deliberately left stale after a switch, so resuming from it
    would put the picker on a runtime that executes nothing (MUL-5163).
  - PUT /api/agent-builder/sessions/{id}/draft stores the configuration,
    including the edits the user typed but never sent. Migration 251 adds
    agent_builder_draft (no FK per repo rule; pruned explicitly by
    DeleteChatSession, the runtime teardown and the workspace teardown, and
    registered in the workspace-deletion manifest).
  - The payload is opaque to the server: its shape is the studio's AgentDraft,
    validated client-side. Teaching Postgres and the handler about it would
    create a second definition to keep in sync for no gain.

Client:
  - The session id lives in `?session=`, so a refresh, a back/forward and a
    reopened tab land back in the same conversation.
  - Leaving no longer deletes anything. The only destructive path is an
    explicit "discard", confirmed in a dialog, next to the create button.
  - Creating the agent archives the conversation instead of deleting it: it is
    the record of how that agent was designed, and an idle carrier costs
    nothing since usage is booked per task.
  - The configuration autosaves (debounced) and restores on arrival, with the
    applied-assistant-message marker stored alongside it so a restore cannot
    re-apply the last reply over edits made after it.
  - The 1.5s polling of messages and pending-task is gone. The global realtime
    sync already invalidates both per session id, exactly as it does for the
    main chat window, which has never polled.
  - The `<agent_draft>` block collapses to one "configuration updated" line.
    The regex now also swallows an unterminated block, which is what streaming
    produces — the raw payload used to scroll past on every turn.

The 2185-line agent-creation-studio.tsx is split into three routes
(`/agents/new`, `/agents/new/manual`, `/agents/new/ai`), its pure logic moves
to packages/core/agents/ with its tests, and the unreachable template flow —
`setMode("templates")` had no caller — is removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(agents): let the builder panes resize

The conversation / configuration split was not draggable. Two structural
reasons, both fixed by giving the group the same shape the chat page uses:

  - The panels reached the group through BuilderWorkspace's fragment, so they
    were not children the group could measure.
  - The group's children alternated between one panel (runtime setup) and two
    (conversation), under one persisted layout id.

The group now lives inside BuilderWorkspace with its two panels as its only
children, and the setup screen renders no group at all — it has nothing to
split.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): give the resize handle a cursor on hover

The separator had no cursor of its own, so the only signal that a split was
draggable arrived after the drag started — the library writes a global
`cursor: ... !important` while dragging, and nothing before it.

Fixed on the shared handle rather than at one call site: every split surface
(chat, inbox, issue detail, project detail, the agent builder) was missing the
same affordance. The library's drag-time rule still outranks this one, so the
cursor keeps narrowing to `e-resize` / `w-resize` once a panel hits its bound.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(agents): render the builder's draft block as an inspectable row

Every builder reply ends in an <agent_draft> block that rewrites the form on
the right. Flattening it to a line of prose said that something changed but not
what, and the payload — the only record of what the builder actually claimed —
was unreachable.

A settled reply now carries a full-width row saying the configuration was
updated, which opens the exact payload. A streaming one keeps a text line
instead: the block is still being written, so there is nothing complete to
open, and without the line the half-finished JSON scrolls past.

ChatMessageList gains an optional `renderAssistantAddon`. It is opt-in per
surface and undefined everywhere but this one, because no other chat speaks
this protocol — the alternative was to keep pushing an embedded protocol
through `transformContent`, which can only ever produce prose.

`extractBuilderDraftBlock` returns an unparseable payload verbatim rather than
withholding it: a malformed block is exactly when someone wants to read it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(agents): address review blockers on the resumable builder

1. Migration prefix collision. `251_agent_runtime_unbind` landed on main after
   this branch cut, so the backend's prefix-uniqueness guard failed. Renumbered
   to 252.

2. #6287 — the manual form still lost everything. The route split moved where
   you land, not what survives: the draft was `useState`, so a tab switch (the
   desktop shell mounts only the active tab) remounted it empty, and the
   beforeunload guard covered a hard reload and nothing else. It now persists
   through the repo's draft-store factory, scoped by what is being created — a
   blank agent and a copy of agent X are different work, and a copy of X is not
   a copy of Y — cleared once the agent is committed, and registered for logout
   / workspace-delete cleanup.

3. A saved draft with no messages was unreachable. The configuration form is
   editable from the moment a builder session exists and autosaves, so someone
   could open it, type a name and leave before the first turn; the list keyed
   "is this a draft" on messages alone, so that row existed and nothing could
   reach it. A session now qualifies on a message OR a stored draft, and sorts
   by whichever it has.

4. The debounce dropped the last edits. Its timer died with the component, so
   navigating away inside the 800ms window lost exactly the keystrokes the user
   had just made. The pending payload is now flushed on unmount.

`useUnsavedDraftWarning` is gone with its last caller: both routes persist, so
the browser prompt would have been warning about work that is already saved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): stop the resize cursor flipping mid-drag

The library narrows its cursor the moment a panel hits a bound — col-resize
while both directions are open, a one-way arrow once only one is. Truthful, but
it reads as a glitch: the icon changes under your hand halfway through a drag
you never stopped making.

`disableCursor` turns that global rule off; the handle's own `cursor-col-resize`
is now the only source. A drag captures the pointer and walks it across the
panels, away from the 8px handle, so the group carries the same cursor for as
long as a separator is active — otherwise it would fall back to a text caret the
instant the pointer left the handle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(agents): key manual drafts by owner, drop the draft-block rendering

Two changes.

One slot destroyed the other flow's work. The manual draft was stored under a
single key: opening a blank form, or a copy of a different agent, refused to
adopt the stored draft and then immediately wrote its own empty form over it —
so a half-finished copy of agent A died the moment the user opened anything
else, before typing a character. Drafts are now keyed by what is being created,
the same shape the chat composer uses for its per-session drafts, and a slot is
dropped when its content is gone rather than parked blank (which also stops the
map growing a dead key per agent ever opened for duplication). Committing an
agent clears that flow's slot only.

The `<agent_draft>` block goes back to being hidden outright. Labelling it and
opening its payload dressed up machinery as content: the block drives the
configuration form, and the form is where its effect is already visible.
`renderAssistantAddon` goes with it — ChatMessageList is back to what it was,
since no surface needs the slot. The two-pattern strip stays: an unterminated
block is what streaming produces, and without matching it the raw JSON scrolled
past the reader on every turn.

Also removed six barrel exports nothing imported through, and unexported five
types only their own file used.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(agents): keep a manual draft whose only edit is a picker

The "is this worth storing" predicate listed six fields by name, and the draft
serializes eleven. A form whose only change was the model, the thinking level,
the service tier, the access scope or a team grant read as untouched, so the
next save deleted its slot — picking a model before typing a name and switching
tabs lost the model.

Enumerating was the mistake, not the specific omissions: the predicate stops
covering every field added after it is written, and the failure is invisible
because each field saves correctly as long as some *other* field is also set.
It now compares the whole draft against a fresh one. The runtime stays outside
that comparison, on the entry rather than in the draft, because the form seeds
it on every visit and counting it would store a draft for a form nobody
touched.

Covered field by field, one edit at a time, so a future field cannot quietly
fall out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 19:00:00 +08:00

546 lines
18 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 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
)
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 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_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_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
)
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_runtimes AS MATERIALIZED (
SELECT id FROM agent_runtime 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_tasks AS MATERIALIZED (
SELECT id
FROM agent_task_queue
WHERE agent_id IN (SELECT id FROM ws_agents)
OR issue_id IN (SELECT id FROM ws_issues)
OR runtime_id IN (SELECT id FROM ws_runtimes)
),
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_usage AS (
DELETE FROM task_usage
WHERE task_id IN (SELECT id FROM ws_tasks)
),
deleted_task_messages AS (
DELETE FROM task_message
WHERE task_id IN (SELECT id FROM ws_tasks)
),
deleted_task_tokens AS (
DELETE FROM task_token
WHERE workspace_id = $1
OR task_id IN (SELECT id FROM ws_tasks)
OR agent_id IN (SELECT id FROM ws_agents)
),
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)
OR task_id IN (SELECT id FROM ws_tasks)
),
deleted_lark_outbound_cards AS (
DELETE FROM lark_outbound_card_message
WHERE chat_session_id IN (SELECT id FROM ws_sessions)
OR task_id IN (SELECT id FROM ws_tasks)
),
deleted_draft_restores AS (
DELETE FROM chat_draft_restore
WHERE chat_session_id IN (SELECT id FROM ws_sessions)
OR task_id IN (SELECT id FROM ws_tasks)
),
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_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_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_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
`
// 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 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 deleteWorkspaceTasks = `-- name: DeleteWorkspaceTasks :exec
DELETE FROM agent_task_queue
WHERE agent_id IN (SELECT id FROM agent WHERE agent.workspace_id = $1)
OR issue_id IN (SELECT id FROM issue WHERE issue.workspace_id = $1)
OR runtime_id IN (SELECT id FROM agent_runtime WHERE agent_runtime.workspace_id = $1)
`
func (q *Queries) DeleteWorkspaceTasks(ctx context.Context, workspaceID pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteWorkspaceTasks, workspaceID)
return err
}
const lockTaskUsageRollupForWorkspaceDelete = `-- name: LockTaskUsageRollupForWorkspaceDelete :exec
SELECT pg_advisory_xact_lock(4246)
`
// The hourly rollup uses session advisory lock 4246. The transaction-scoped
// lock shares that namespace: an in-flight rollup finishes first, then no new
// rollup can write the workspace's aggregates until this delete commits.
func (q *Queries) LockTaskUsageRollupForWorkspaceDelete(ctx context.Context) error {
_, err := q.db.Exec(ctx, lockTaskUsageRollupForWorkspaceDelete)
return err
}
const prepareWorkspaceDeletionLinks = `-- name: PrepareWorkspaceDeletionLinks :exec
WITH
ws_agents AS MATERIALIZED (
SELECT id FROM agent WHERE agent.workspace_id = $1
),
ws_runtimes AS MATERIALIZED (
SELECT id FROM agent_runtime WHERE agent_runtime.workspace_id = $1
),
ws_issues AS MATERIALIZED (
SELECT id FROM issue WHERE issue.workspace_id = $1
),
ws_tasks AS MATERIALIZED (
-- Keep all three ownership paths until the application enforces that a
-- task's agent/runtime/issue always belong to the same workspace. The OR
-- can scan globally, but simplifying it before that invariant exists
-- would turn a performance optimization into a tenant-cleanup bug.
SELECT id
FROM agent_task_queue
WHERE agent_id IN (SELECT id FROM ws_agents)
OR issue_id IN (SELECT id FROM ws_issues)
OR runtime_id IN (SELECT id FROM ws_runtimes)
),
detached_tasks AS (
UPDATE agent_task_queue
SET parent_task_id = NULL,
autopilot_run_id = NULL
WHERE id IN (SELECT id FROM ws_tasks)
AND (parent_task_id IS NOT NULL OR autopilot_run_id IS NOT NULL)
),
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.
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
}