|
| 1 | +-- Deleting a workspace (#1158) is a soft delete. The organization row stays, |
| 2 | +-- so projects, audit rows, and usage still point at it, but it has no |
| 3 | +-- members and its short name is released. Its projects are soft deleted |
| 4 | +-- too: a deleted project leaves every list and picker, and its name can be |
| 5 | +-- used again. A Thread in a deleted project stays with its owner, archived |
| 6 | +-- and read-only. |
| 7 | +ALTER TABLE organization ADD COLUMN deletedAt TEXT; |
| 8 | + |
| 9 | +ALTER TABLE projects ADD COLUMN deleted_at TEXT; |
| 10 | + |
| 11 | +-- Project reads never see a deleted project. |
| 12 | +DROP VIEW project_read_model; |
| 13 | +CREATE VIEW project_read_model AS |
| 14 | +SELECT projects.id, projects.owner_user_id, projects.workspace_id, projects.name, |
| 15 | + projects.description, projects.icon_key, projects.revision, |
| 16 | + project_repository.provider AS repository_provider, |
| 17 | + project_repository.binding_revision AS repository_binding_revision, |
| 18 | + project_repository.full_name AS repository_full_name, |
| 19 | + project_repository.web_url AS repository_web_url, |
| 20 | + project_repository.clone_url AS repository_clone_url, |
| 21 | + projects.ship_action, projects.commit_author_preference, |
| 22 | + projects.commit_author_name, projects.commit_author_email, |
| 23 | + projects.signing_preference, projects.runner_profile_id, |
| 24 | + projects.public_code_enabled, projects.created_at, projects.updated_at, |
| 25 | + (SELECT json_group_array(json_object( |
| 26 | + 'provider', additional.provider, 'fullName', additional.full_name, |
| 27 | + 'webUrl', additional.web_url, 'cloneUrl', additional.clone_url)) |
| 28 | + FROM (SELECT * FROM project_additional_repository |
| 29 | + WHERE project_id = projects.id ORDER BY position) AS additional |
| 30 | + ) AS additional_repositories_json, |
| 31 | + projects.deleted_at |
| 32 | +FROM projects LEFT JOIN project_repository ON project_repository.project_id = projects.id |
| 33 | +WHERE projects.deleted_at IS NULL; |
| 34 | + |
| 35 | +DROP INDEX projects_personal_name_unique_idx; |
| 36 | +CREATE UNIQUE INDEX projects_personal_name_unique_idx |
| 37 | + ON projects(owner_user_id, name COLLATE NOCASE) |
| 38 | + WHERE workspace_id IS NULL AND name != 'No Project' AND deleted_at IS NULL; |
| 39 | + |
| 40 | +DROP INDEX projects_workspace_name_unique_idx; |
| 41 | +CREATE UNIQUE INDEX projects_workspace_name_unique_idx |
| 42 | + ON projects(workspace_id, name COLLATE NOCASE) |
| 43 | + WHERE workspace_id IS NOT NULL AND name != 'No Project' AND deleted_at IS NULL; |
| 44 | + |
| 45 | +DROP INDEX projects_projectless_personal_unique_idx; |
| 46 | +CREATE UNIQUE INDEX projects_projectless_personal_unique_idx |
| 47 | + ON projects(owner_user_id) |
| 48 | + WHERE workspace_id IS NULL AND name = 'No Project' AND deleted_at IS NULL; |
| 49 | + |
| 50 | +DROP INDEX projects_projectless_workspace_unique_idx; |
| 51 | +CREATE UNIQUE INDEX projects_projectless_workspace_unique_idx |
| 52 | + ON projects(owner_user_id, workspace_id) |
| 53 | + WHERE workspace_id IS NOT NULL AND name = 'No Project' AND deleted_at IS NULL; |
| 54 | + |
| 55 | +-- The owner may hand the workspace to another member: the new owner is |
| 56 | +-- promoted first, then the old owner steps down. A workspace never loses |
| 57 | +-- its last owner. |
| 58 | +DROP TRIGGER workspace_owner_role_immutable_before_update; |
| 59 | +CREATE TRIGGER workspace_owner_role_immutable_before_update |
| 60 | +BEFORE UPDATE OF role ON member |
| 61 | +WHEN OLD.role = 'owner' AND NEW.role <> 'owner' |
| 62 | + AND ( |
| 63 | + SELECT count(*) |
| 64 | + FROM member |
| 65 | + WHERE organizationId = OLD.organizationId |
| 66 | + AND role = 'owner' |
| 67 | + ) <= 1 |
| 68 | +BEGIN |
| 69 | + SELECT RAISE(ABORT, 'workspace owner is immutable'); |
| 70 | +END; |
| 71 | + |
| 72 | +-- What a Thread asks of its owner, listed in their Needs-you inbox. The |
| 73 | +-- agent's request_attention tool writes a 'question' or an 'approval' with a |
| 74 | +-- one-line ask and an optional link; a root Thread's failed turn writes |
| 75 | +-- 'failed' when nothing else is asked. The Thread's next turn starting, or |
| 76 | +-- the owner's Dismiss, deletes the row. |
| 77 | +CREATE TABLE thread_attention_request ( |
| 78 | + thread_id TEXT PRIMARY KEY REFERENCES threads (id) ON DELETE CASCADE, |
| 79 | + kind TEXT NOT NULL CHECK (kind IN ('question', 'approval', 'failed')), |
| 80 | + summary TEXT CHECK (summary IS NULL OR length(summary) BETWEEN 1 AND 300), |
| 81 | + url TEXT CHECK (url IS NULL OR length(url) BETWEEN 1 AND 2048), |
| 82 | + requested_at TEXT NOT NULL CHECK (length(requested_at) BETWEEN 20 AND 40) |
| 83 | +); |
| 84 | + |
| 85 | +-- The GitHub issues and pull requests a Thread works on. A comment |
| 86 | +-- subscription on one links it, as does the agent's `link_github_issue` |
| 87 | +-- tool; the link outlives the subscription. The Thread header lists them, |
| 88 | +-- and find_thread's `issue:owner/repo#N` finds the Thread from one. |
| 89 | +-- Webhooks keep the title and state current; no view reads GitHub. |
| 90 | +CREATE TABLE thread_github_link ( |
| 91 | + thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE, |
| 92 | + -- owner/name as GitHub spells it; repository_key is it lowercased. |
| 93 | + repository TEXT NOT NULL CHECK (length(repository) BETWEEN 3 AND 200), |
| 94 | + repository_key TEXT NOT NULL CHECK (repository_key = lower(repository)), |
| 95 | + number INTEGER NOT NULL CHECK (number > 0), |
| 96 | + kind TEXT NOT NULL CHECK (kind IN ('issue', 'pull')), |
| 97 | + subject_json TEXT CHECK ( |
| 98 | + subject_json IS NULL |
| 99 | + OR (json_valid(subject_json) AND length(subject_json) <= 2000) |
| 100 | + ), |
| 101 | + created_at TEXT NOT NULL, |
| 102 | + PRIMARY KEY (thread_id, repository_key, number) |
| 103 | +); |
| 104 | + |
| 105 | +CREATE INDEX thread_github_link_target_idx |
| 106 | +ON thread_github_link (repository_key, number); |
| 107 | + |
| 108 | +-- Threads that already follow an issue's or a PR's comments. |
| 109 | +INSERT OR IGNORE INTO thread_github_link ( |
| 110 | + thread_id, repository, repository_key, number, kind, subject_json, |
| 111 | + created_at |
| 112 | +) |
| 113 | +SELECT thread_id, repository, lower(repository), |
| 114 | + CAST(json_extract(filter_json, |
| 115 | + CASE filter_kind WHEN 'pr-comments' THEN '$.prNumber' |
| 116 | + ELSE '$.issueNumber' END) AS INTEGER), |
| 117 | + CASE filter_kind WHEN 'pr-comments' THEN 'pull' ELSE 'issue' END , |
| 118 | + subject_json, |
| 119 | + created_at |
| 120 | + FROM github_subscription |
| 121 | + WHERE destination = 'this-thread' |
| 122 | + AND filter_kind IN ('pr-comments', 'issue-comments') |
| 123 | + AND CAST(json_extract(filter_json, |
| 124 | + CASE filter_kind WHEN 'pr-comments' THEN '$.prNumber' |
| 125 | + ELSE '$.issueNumber' END) AS INTEGER) > 0; |
| 126 | + |
| 127 | +-- "Add dx as co-author" (#1303). A project snapshots it at creation, like |
| 128 | +-- its commit author; when it is on and the person authors commits, each |
| 129 | +-- commit gets a `Co-authored-by: dxcodeagent <agent@dxcode.dev>` trailer. |
| 130 | +-- Existing projects keep crediting dx. Defaults store NULL until someone |
| 131 | +-- chooses, which resolves to on. |
| 132 | +ALTER TABLE projects |
| 133 | + ADD COLUMN dx_co_author INTEGER NOT NULL DEFAULT 1 |
| 134 | + CHECK (dx_co_author IN (0, 1)); |
| 135 | + |
| 136 | +ALTER TABLE personal_project_defaults |
| 137 | + ADD COLUMN dx_co_author INTEGER |
| 138 | + CHECK (dx_co_author IS NULL OR dx_co_author IN (0, 1)); |
| 139 | + |
| 140 | +ALTER TABLE workspace_project_defaults |
| 141 | + ADD COLUMN dx_co_author INTEGER |
| 142 | + CHECK (dx_co_author IS NULL OR dx_co_author IN (0, 1)); |
| 143 | + |
| 144 | +DROP VIEW project_read_model; |
| 145 | +CREATE VIEW project_read_model AS |
| 146 | +SELECT projects.id, projects.owner_user_id, projects.workspace_id, projects.name, |
| 147 | + projects.description, projects.icon_key, projects.revision, |
| 148 | + project_repository.provider AS repository_provider, |
| 149 | + project_repository.binding_revision AS repository_binding_revision, |
| 150 | + project_repository.full_name AS repository_full_name, |
| 151 | + project_repository.web_url AS repository_web_url, |
| 152 | + project_repository.clone_url AS repository_clone_url, |
| 153 | + projects.ship_action, projects.commit_author_preference, |
| 154 | + projects.commit_author_name, projects.commit_author_email, |
| 155 | + projects.signing_preference, projects.runner_profile_id, |
| 156 | + projects.public_code_enabled, projects.created_at, projects.updated_at, |
| 157 | + (SELECT json_group_array(json_object( |
| 158 | + 'provider', additional.provider, 'fullName', additional.full_name, |
| 159 | + 'webUrl', additional.web_url, 'cloneUrl', additional.clone_url)) |
| 160 | + FROM (SELECT * FROM project_additional_repository |
| 161 | + WHERE project_id = projects.id ORDER BY position) AS additional |
| 162 | + ) AS additional_repositories_json, |
| 163 | + projects.deleted_at, |
| 164 | + projects.dx_co_author |
| 165 | +FROM projects LEFT JOIN project_repository ON project_repository.project_id = projects.id |
| 166 | +WHERE projects.deleted_at IS NULL; |
| 167 | + |
| 168 | +-- The person's desktop sidebar panel, shared by every device they sign in on |
| 169 | +-- and returned with their sidebar layout (`sidebar_layout_entry`): the width |
| 170 | +-- they dragged it to (NULL until they resize it, so the default applies) and |
| 171 | +-- whether they keep it collapsed. Without a row the sidebar opens at its |
| 172 | +-- default width; a width the current schema no longer accepts falls back to |
| 173 | +-- the default too. |
| 174 | +CREATE TABLE sidebar_panel ( |
| 175 | + user_id TEXT PRIMARY KEY NOT NULL, |
| 176 | + width INTEGER CHECK (width IS NULL OR width BETWEEN 1 AND 4096), |
| 177 | + collapsed INTEGER NOT NULL DEFAULT 0 CHECK (collapsed IN (0, 1)), |
| 178 | + updated_at TEXT NOT NULL |
| 179 | +) WITHOUT ROWID; |
| 180 | + |
| 181 | +-- When E2B refuses a Thread's create with a client error it makes no |
| 182 | +-- sandbox, and the resolver returns the attempt to 'uninitialized' so the |
| 183 | +-- next request creates again. The transition trigger rejected that step, so |
| 184 | +-- the record stayed 'provisioning' with no sandbox and every later request |
| 185 | +-- failed. Allow it only back to an empty record. |
| 186 | +DROP TRIGGER execution_workspace_transition_before_update; |
| 187 | + |
| 188 | +CREATE TRIGGER execution_workspace_transition_before_update |
| 189 | +BEFORE UPDATE ON execution_workspace |
| 190 | +WHEN NEW.state IS NOT OLD.state |
| 191 | + AND NOT ( |
| 192 | + (OLD.state = 'uninitialized' AND NEW.state = 'provisioning') |
| 193 | + OR (OLD.state = 'provisioning' AND NEW.state IN ('initialized', 'conflict')) |
| 194 | + OR (OLD.state = 'provisioning' AND NEW.state = 'uninitialized' |
| 195 | + AND OLD.provider_sandbox_id IS NULL |
| 196 | + AND NEW.provider_sandbox_id IS NULL |
| 197 | + AND NEW.initialization_attempt_id IS NULL) |
| 198 | + OR (OLD.state = 'legacy' AND NEW.state IN ('initialized', 'legacy_unavailable', 'conflict')) |
| 199 | + OR (OLD.state = 'initialized' AND NEW.state = 'lost') |
| 200 | + ) |
| 201 | +BEGIN |
| 202 | + SELECT RAISE(ABORT, 'invalid execution workspace transition'); |
| 203 | +END; |
0 commit comments