mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-05 09:30:05 +02:00
* 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>
546 lines
18 KiB
Go
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
|
|
}
|