@agent-native/core 0.136.5 → 0.137.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/app/components/AgentCompletionSound.tsx +105 -0
  3. package/corpus/templates/analytics/app/components/layout/Layout.tsx +44 -35
  4. package/corpus/templates/analytics/app/i18n-data.ts +38 -0
  5. package/corpus/templates/analytics/app/pages/Settings.tsx +53 -1
  6. package/corpus/templates/analytics/app/pages/settings/settings-search.ts +6 -0
  7. package/corpus/templates/analytics/changelog/2026-08-04-choose-whether-analytics-plays-a-bell-when-the-agent-finishe.md +6 -0
  8. package/corpus/templates/analytics/shared/analytics-user-prefs.ts +2 -0
  9. package/corpus/templates/clips/app/components/import-menu.tsx +109 -0
  10. package/corpus/templates/clips/app/components/library/library-grid.tsx +8 -18
  11. package/corpus/templates/clips/app/components/library/library-layout.tsx +14 -50
  12. package/corpus/templates/clips/app/components/library/sort-menu.tsx +20 -6
  13. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +10 -34
  14. package/corpus/templates/clips/app/routes/_app.library._index.tsx +0 -2
  15. package/corpus/templates/clips/changelog/2026-08-04-cleaner-library-controls-with-an-icon-only-sort-button.md +6 -0
  16. package/corpus/templates/clips/changelog/2026-08-04-upload-and-loom-imports-now-live-under-one-import-menu.md +6 -0
  17. package/corpus/templates/content/actions/_database-utils.ts +6 -0
  18. package/corpus/templates/content/actions/bind-content-database-source-field.ts +273 -97
  19. package/corpus/templates/content/actions/change-content-database-source-role.ts +65 -29
  20. package/corpus/templates/content/actions/configure-document-property.ts +113 -32
  21. package/corpus/templates/content/actions/delete-content-database.ts +5 -7
  22. package/corpus/templates/content/actions/delete-document-property.ts +109 -62
  23. package/corpus/templates/content/actions/delete-document.ts +178 -36
  24. package/corpus/templates/content/actions/duplicate-database-item.ts +27 -0
  25. package/corpus/templates/content/actions/duplicate-database-items.ts +31 -1
  26. package/corpus/templates/content/actions/permanently-delete-document.ts +4 -6
  27. package/corpus/templates/content/actions/remove-database-items.ts +13 -0
  28. package/corpus/templates/content/actions/set-document-property.ts +69 -1
  29. package/corpus/templates/content/actions/upsert-database-item-by-key.ts +814 -0
  30. package/corpus/templates/content/parity/matrix.md +1 -1
  31. package/corpus/templates/content/parity/matrix.ts +1 -0
  32. package/corpus/templates/content/server/db/schema.ts +27 -0
  33. package/corpus/templates/content/server/plugins/db.ts +24 -0
  34. package/corpus/templates/design/app/components/design/BreakpointBar.tsx +122 -114
  35. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +2 -2
  36. package/corpus/templates/design/app/components/design/EditPanel.tsx +68 -34
  37. package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +78 -4
  38. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +8 -0
  39. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +5 -2
  40. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +10 -9
  41. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +31 -69
  42. package/corpus/templates/design/app/components/design/inspector/design-icons.tsx +80 -106
  43. package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
  44. package/corpus/templates/design/app/hooks/use-editor-preferences.ts +75 -0
  45. package/corpus/templates/design/app/i18n-data.ts +8 -0
  46. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +61 -0
  47. package/corpus/templates/design/app/pages/design-editor/editor-preferences.ts +98 -0
  48. package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +491 -0
  49. package/corpus/templates/design/app/pages/design-editor/paste-placement.ts +133 -0
  50. package/corpus/templates/design/app/pages/design-editor/png-export-render.ts +8 -0
  51. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +5 -12
  52. package/corpus/templates/design/changelog/2026-08-03-arrow-keys-now-reorder-a-layer-inside-an-auto-layout-frame.md +6 -0
  53. package/corpus/templates/design/changelog/2026-08-03-auto-layout-flow-icons-show-the-direction-at-a-glance-instea.md +6 -0
  54. package/corpus/templates/design/changelog/2026-08-03-auto-layout-icons-share-one-outlined-square-style-so-flow-ga.md +6 -0
  55. package/corpus/templates/design/changelog/2026-08-03-export-has-a-named-preview-section-you-can-expand-replacing-.md +6 -0
  56. package/corpus/templates/design/changelog/2026-08-03-export-preview-shows-the-selected-element-instead-of-the-who.md +6 -0
  57. package/corpus/templates/design/changelog/2026-08-03-inspector-sections-show-an-expand-indicator-so-it-is-clear-w.md +6 -0
  58. package/corpus/templates/design/changelog/2026-08-03-paste-lands-inside-a-selected-frame-instead-of-beside-it.md +6 -0
  59. package/corpus/templates/design/changelog/2026-08-03-the-add-breakpoint-button-sits-outside-the-breakpoint-select.md +6 -0
  60. package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-keep-space-below-the-continu.md +6 -0
  61. package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-share-the-chat-panel-s-backg.md +6 -0
  62. package/corpus/templates/design/changelog/2026-08-03-the-export-panel-no-longer-stacks-a-duplicate-export-block-o.md +6 -0
  63. package/corpus/templates/design/changelog/2026-08-03-the-text-tool-uses-a-t-icon-matching-the-letter-it-inserts.md +6 -0
  64. package/corpus/templates/factory/.env.example +17 -0
  65. package/corpus/templates/factory/AGENTS.md +27 -17
  66. package/corpus/templates/factory/README.md +19 -9
  67. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +90 -0
  68. package/corpus/templates/factory/actions/get-triage-config.ts +14 -3
  69. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +386 -0
  70. package/corpus/templates/factory/actions/list-factory-automations.ts +53 -0
  71. package/corpus/templates/factory/actions/list-triage-items.ts +7 -2
  72. package/corpus/templates/factory/actions/poll-github-sources.ts +203 -0
  73. package/corpus/templates/factory/actions/poll-sentry-errors.ts +161 -0
  74. package/corpus/templates/factory/actions/poll-slack-channel.ts +4 -2
  75. package/corpus/templates/factory/actions/run-factory-automation.ts +42 -0
  76. package/corpus/templates/factory/actions/save-factory-automation.ts +69 -0
  77. package/corpus/templates/factory/actions/save-triage-config.ts +28 -2
  78. package/corpus/templates/factory/actions/start-builder-for-item.ts +411 -0
  79. package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +14 -3
  80. package/corpus/templates/factory/app/components/ui/checkbox.tsx +1 -0
  81. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +7 -0
  82. package/corpus/templates/factory/app/i18n/en-US.ts +35 -1
  83. package/corpus/templates/factory/app/routes/factory.tsx +462 -9
  84. package/corpus/templates/factory/changelog/2026-08-04-added-organization-owned-factory-automations-for-slack-bug-t.md +6 -0
  85. package/corpus/templates/factory/changelog/2026-08-04-automation-editor-controls-and-model-picker.md +6 -0
  86. package/corpus/templates/factory/netlify.toml +2 -2
  87. package/corpus/templates/factory/server/connectors/credentials.ts +6 -5
  88. package/corpus/templates/factory/server/connectors/slack.ts +133 -5
  89. package/corpus/templates/factory/server/db/schema.ts +6 -0
  90. package/corpus/templates/factory/server/lib/require-factory-automation.ts +65 -0
  91. package/corpus/templates/factory/server/plugins/agent-chat.ts +29 -11
  92. package/corpus/templates/factory/server/plugins/auth.ts +1 -0
  93. package/corpus/templates/factory/server/plugins/factory-migrations.ts +12 -0
  94. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +218 -67
  95. package/corpus/templates/factory/server/triage/builder-executor.ts +4 -0
  96. package/corpus/templates/factory/server/triage/contracts.ts +14 -1
  97. package/corpus/templates/factory/server/triage/github-client.ts +404 -0
  98. package/corpus/templates/factory/server/triage/metadata.ts +44 -0
  99. package/corpus/templates/factory/server/triage/pr-babysit.ts +5 -5
  100. package/corpus/templates/factory/server/triage/pr-policy.ts +144 -0
  101. package/corpus/templates/factory/server/triage/sentry-client.ts +180 -0
  102. package/corpus/templates/factory/server/triage/slack-client.ts +43 -0
  103. package/corpus/templates/factory/server/triage/slack-poller.ts +9 -2
  104. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +27 -0
  105. package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +14 -4
  106. package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -0
  107. package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -0
  108. package/corpus/templates/plan/actions/create-plan-design.ts +8 -6
  109. package/corpus/templates/plan/actions/create-prototype-plan.ts +8 -6
  110. package/corpus/templates/plan/actions/create-ui-plan.ts +7 -7
  111. package/corpus/templates/plan/actions/get-plan-blocks.ts +7 -1
  112. package/corpus/templates/plan/actions/validate-plan-input.ts +39 -0
  113. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +167 -11
  114. package/corpus/templates/plan/app/i18n/en-US.ts +1 -0
  115. package/corpus/templates/plan/changelog/2026-08-04-canvas-zoom-controls-now-explain-the-command-ctrl-plus-scrol.md +6 -0
  116. package/corpus/templates/plan/changelog/2026-08-04-pinch-to-zoom-is-now-supported-on-touchscreens-in-visual-pla.md +6 -0
  117. package/corpus/templates/plan/shared/plan-content.ts +17 -2
  118. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +321 -0
  119. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +32 -11
  120. package/corpus/templates/slides/app/lib/recent-references.ts +82 -0
  121. package/corpus/templates/slides/app/pages/DeckEditor.tsx +3 -0
  122. package/corpus/templates/slides/app/pages/Index.tsx +236 -181
  123. package/dist/cli/skills-content/canvas.d.ts +3 -3
  124. package/dist/cli/skills-content/canvas.js +16 -5
  125. package/dist/cli/skills-content/visual-plan-skill.d.ts +1 -1
  126. package/dist/cli/skills-content/visual-plan-skill.js +27 -0
  127. package/dist/cli/skills-content/wireframe.d.ts +2 -2
  128. package/dist/cli/skills-content/wireframe.js +7 -0
  129. package/dist/client/AgentPanel.js +23 -8
  130. package/dist/client/DefaultSpinner.d.ts +0 -12
  131. package/dist/client/DefaultSpinner.js +22 -2
  132. package/dist/client/api-path.d.ts +6 -0
  133. package/dist/client/api-path.js +55 -0
  134. package/dist/client/onboarding/FirstRunOnboarding.d.ts +3 -0
  135. package/dist/client/onboarding/FirstRunOnboarding.js +93 -0
  136. package/dist/client/onboarding/first-run-enabled.d.ts +2 -0
  137. package/dist/client/onboarding/first-run-enabled.js +9 -0
  138. package/dist/client/onboarding/index.d.ts +2 -1
  139. package/dist/client/onboarding/index.js +1 -0
  140. package/dist/client/onboarding/use-onboarding.d.ts +6 -1
  141. package/dist/client/onboarding/use-onboarding.js +35 -1
  142. package/dist/client/route-state.js +10 -0
  143. package/dist/collab/awareness.d.ts +2 -2
  144. package/dist/collab/routes.d.ts +1 -1
  145. package/dist/connections/catalog.d.ts +4 -4
  146. package/dist/connections/catalog.js +3 -3
  147. package/dist/db/index.d.ts +1 -1
  148. package/dist/db/index.js +1 -1
  149. package/dist/deploy/build.js +2 -1
  150. package/dist/notifications/routes.d.ts +3 -3
  151. package/dist/observability/routes.d.ts +1 -1
  152. package/dist/onboarding/app-profile.d.ts +4 -0
  153. package/dist/onboarding/app-profile.js +378 -0
  154. package/dist/onboarding/index.d.ts +2 -1
  155. package/dist/onboarding/index.js +1 -0
  156. package/dist/onboarding/plugin.d.ts +2 -0
  157. package/dist/onboarding/plugin.js +21 -1
  158. package/dist/onboarding/types.d.ts +19 -0
  159. package/dist/progress/routes.d.ts +1 -1
  160. package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
  161. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  162. package/dist/secrets/routes.d.ts +3 -3
  163. package/dist/server/auth.js +21 -1
  164. package/dist/server/better-auth-instance.d.ts +3 -2
  165. package/dist/server/better-auth-instance.js +9 -7
  166. package/dist/server/email-markdown.js +3 -8
  167. package/dist/shared/first-run-onboarding.d.ts +3 -0
  168. package/dist/shared/first-run-onboarding.js +3 -0
  169. package/dist/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +25 -9
  170. package/dist/triggers/index.d.ts +3 -0
  171. package/dist/triggers/index.js +5 -0
  172. package/docs/content/cross-app-sso.mdx +26 -0
  173. package/package.json +1 -1
  174. package/src/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +25 -9
@@ -1,7 +1,7 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { writeAppState } from "@agent-native/core/application-state";
3
3
  import { assertAccess } from "@agent-native/core/sharing";
4
- import { and, eq, inArray, isNotNull, isNull, ne, or } from "drizzle-orm";
4
+ import { and, eq, inArray, isNotNull, isNull, ne, or, sql } from "drizzle-orm";
5
5
  import { z } from "zod";
6
6
 
7
7
  import { getDb, schema } from "../server/db/index.js";
@@ -97,6 +97,28 @@ async function selectDatabaseItemDocuments(
97
97
  return ownedRows.map((row) => ({ documentId: row.id }));
98
98
  }
99
99
 
100
+ async function selectMembershipDatabaseIds(
101
+ db: ReturnType<typeof getDb>,
102
+ documentIds: string[],
103
+ ownerEmail: string,
104
+ ) {
105
+ const rows: Array<{ databaseId: string }> = [];
106
+ for (const batch of chunks(documentIds, DELETE_BATCH_SIZE)) {
107
+ rows.push(
108
+ ...(await db
109
+ .select({ databaseId: schema.contentDatabaseItems.databaseId })
110
+ .from(schema.contentDatabaseItems)
111
+ .where(
112
+ and(
113
+ inArray(schema.contentDatabaseItems.documentId, batch),
114
+ eq(schema.contentDatabaseItems.ownerEmail, ownerEmail),
115
+ ),
116
+ )),
117
+ );
118
+ }
119
+ return [...new Set(rows.map((row) => row.databaseId))];
120
+ }
121
+
100
122
  async function collectDocumentSubtreeForDelete(
101
123
  db: ReturnType<typeof getDb>,
102
124
  rootId: string,
@@ -150,22 +172,83 @@ async function collectDocumentSubtreeForDelete(
150
172
  frontier = [...next];
151
173
  }
152
174
 
175
+ const collectedDocumentIds = [...documentIds];
176
+ const collectedOwnedDatabaseIds = [...ownedDatabaseIds];
153
177
  return {
154
- documentIds: [...documentIds],
155
- ownedDatabaseIds: [...ownedDatabaseIds],
178
+ documentIds: collectedDocumentIds,
179
+ ownedDatabaseIds: collectedOwnedDatabaseIds,
180
+ lockDatabaseIds: [
181
+ ...new Set([
182
+ ...collectedOwnedDatabaseIds,
183
+ ...(await selectMembershipDatabaseIds(
184
+ db,
185
+ collectedDocumentIds,
186
+ ownerEmail,
187
+ )),
188
+ ]),
189
+ ],
156
190
  };
157
191
  }
158
192
 
193
+ async function lockDatabasesAndRecollect<
194
+ T extends { lockDatabaseIds: string[] },
195
+ >(
196
+ db: ReturnType<typeof getDb>,
197
+ ownerEmail: string,
198
+ collect: () => Promise<T>,
199
+ ): Promise<T> {
200
+ const lockedDatabaseIds = new Set<string>();
201
+ let collected = await collect();
202
+ while (true) {
203
+ const unlockedDatabaseIds = collected.lockDatabaseIds.filter(
204
+ (databaseId) => !lockedDatabaseIds.has(databaseId),
205
+ );
206
+ if (unlockedDatabaseIds.length === 0) return collected;
207
+ for (const batch of chunks(unlockedDatabaseIds, DELETE_BATCH_SIZE)) {
208
+ await db
209
+ .update(schema.contentDatabases)
210
+ .set({ updatedAt: sql`${schema.contentDatabases.updatedAt}` })
211
+ .where(
212
+ and(
213
+ inArray(schema.contentDatabases.id, batch),
214
+ eq(schema.contentDatabases.ownerEmail, ownerEmail),
215
+ ),
216
+ );
217
+ for (const databaseId of batch) lockedDatabaseIds.add(databaseId);
218
+ }
219
+ collected = await collect();
220
+ }
221
+ }
222
+
159
223
  export async function trashDocumentSubtree(
160
224
  db: ReturnType<typeof getDb>,
161
225
  id: string,
162
226
  ownerEmail: string,
163
227
  trashedAt = new Date().toISOString(),
164
228
  ): Promise<string[]> {
165
- const { documentIds } = await collectDocumentSubtreeForDelete(
166
- db,
167
- id,
168
- ownerEmail,
229
+ return db.transaction((tx) =>
230
+ trashDocumentSubtreeInTransaction(
231
+ tx as unknown as ReturnType<typeof getDb>,
232
+ id,
233
+ ownerEmail,
234
+ trashedAt,
235
+ ),
236
+ );
237
+ }
238
+
239
+ async function trashDocumentSubtreeInTransaction(
240
+ db: ReturnType<typeof getDb>,
241
+ id: string,
242
+ ownerEmail: string,
243
+ trashedAt: string,
244
+ ): Promise<string[]> {
245
+ // Stable-key upserts lock their canonical database row before creating or
246
+ // updating children. Acquire the same locks, then collect again: an upsert
247
+ // that won the lock before this transaction may have added a child after the
248
+ // first traversal, while one that arrives later must wait until the database
249
+ // has been marked deleted and will fail closed.
250
+ const { documentIds } = await lockDatabasesAndRecollect(db, ownerEmail, () =>
251
+ collectDocumentSubtreeForDelete(db, id, ownerEmail),
169
252
  );
170
253
  await assertNotWorkspaceCatalogDocuments(db, documentIds, "deleted");
171
254
 
@@ -316,14 +399,20 @@ export async function deleteDocumentRecursive(
316
399
  id: string,
317
400
  ownerEmail: string,
318
401
  ): Promise<string[]> {
319
- const { documentIds, ownedDatabaseIds } =
320
- await collectDocumentSubtreeForDelete(db, id, ownerEmail);
321
- return deleteCollectedDocuments(
322
- db,
323
- documentIds,
324
- ownedDatabaseIds,
325
- ownerEmail,
326
- );
402
+ return db.transaction(async (tx) => {
403
+ const scopedDb = tx as unknown as ReturnType<typeof getDb>;
404
+ const { documentIds, ownedDatabaseIds } = await lockDatabasesAndRecollect(
405
+ scopedDb,
406
+ ownerEmail,
407
+ () => collectDocumentSubtreeForDelete(scopedDb, id, ownerEmail),
408
+ );
409
+ return deleteCollectedDocuments(
410
+ scopedDb,
411
+ documentIds,
412
+ ownedDatabaseIds,
413
+ ownerEmail,
414
+ );
415
+ });
327
416
  }
328
417
 
329
418
  async function deleteCollectedDocuments(
@@ -416,6 +505,27 @@ async function deleteCollectedDocuments(
416
505
  );
417
506
  });
418
507
 
508
+ await deleteWhereIn(ownedDatabaseIds, async (databaseIdBatch) => {
509
+ await db
510
+ .delete(schema.contentDatabaseItemKeyClaims)
511
+ .where(
512
+ inArray(
513
+ schema.contentDatabaseItemKeyClaims.databaseId,
514
+ databaseIdBatch,
515
+ ),
516
+ );
517
+ });
518
+ await deleteWhereIn(documentIds, async (documentIdBatch) => {
519
+ await db
520
+ .delete(schema.contentDatabaseItemKeyClaims)
521
+ .where(
522
+ inArray(
523
+ schema.contentDatabaseItemKeyClaims.documentId,
524
+ documentIdBatch,
525
+ ),
526
+ );
527
+ });
528
+
419
529
  await deleteWhereIn(documentIds, async (documentIdBatch) => {
420
530
  await db
421
531
  .delete(schema.contentDatabaseBodyHydrationQueue)
@@ -518,6 +628,20 @@ export async function deleteTrashedDocumentSubtree(
518
628
  db: ReturnType<typeof getDb>,
519
629
  id: string,
520
630
  ownerEmail: string,
631
+ ): Promise<string[]> {
632
+ return db.transaction((tx) =>
633
+ deleteTrashedDocumentSubtreeInTransaction(
634
+ tx as unknown as ReturnType<typeof getDb>,
635
+ id,
636
+ ownerEmail,
637
+ ),
638
+ );
639
+ }
640
+
641
+ async function deleteTrashedDocumentSubtreeInTransaction(
642
+ db: ReturnType<typeof getDb>,
643
+ id: string,
644
+ ownerEmail: string,
521
645
  ): Promise<string[]> {
522
646
  const [root] = await db
523
647
  .select({ id: schema.documents.id })
@@ -537,23 +661,43 @@ export async function deleteTrashedDocumentSubtree(
537
661
  );
538
662
  }
539
663
 
540
- const documentIds = (
541
- await db
542
- .select({ id: schema.documents.id })
543
- .from(schema.documents)
544
- .where(
545
- and(
546
- eq(schema.documents.ownerEmail, ownerEmail),
547
- eq(schema.documents.trashRootId, id),
548
- isNotNull(schema.documents.trashedAt),
549
- ),
550
- )
551
- ).map((document) => document.id);
552
- const ownedDatabaseIds = await selectOwnedDatabaseIds(
664
+ const { documentIds, ownedDatabaseIds } = await lockDatabasesAndRecollect(
553
665
  db,
554
- documentIds,
555
666
  ownerEmail,
556
- ).then((rows) => rows.map((database) => database.id));
667
+ async () => {
668
+ const collectedDocumentIds = (
669
+ await db
670
+ .select({ id: schema.documents.id })
671
+ .from(schema.documents)
672
+ .where(
673
+ and(
674
+ eq(schema.documents.ownerEmail, ownerEmail),
675
+ eq(schema.documents.trashRootId, id),
676
+ isNotNull(schema.documents.trashedAt),
677
+ ),
678
+ )
679
+ ).map((document) => document.id);
680
+ const collectedDatabaseIds = await selectOwnedDatabaseIds(
681
+ db,
682
+ collectedDocumentIds,
683
+ ownerEmail,
684
+ ).then((rows) => rows.map((database) => database.id));
685
+ return {
686
+ documentIds: collectedDocumentIds,
687
+ ownedDatabaseIds: collectedDatabaseIds,
688
+ lockDatabaseIds: [
689
+ ...new Set([
690
+ ...collectedDatabaseIds,
691
+ ...(await selectMembershipDatabaseIds(
692
+ db,
693
+ collectedDocumentIds,
694
+ ownerEmail,
695
+ )),
696
+ ]),
697
+ ],
698
+ };
699
+ },
700
+ );
557
701
 
558
702
  await db
559
703
  .update(schema.documents)
@@ -633,12 +777,10 @@ export default defineAction({
633
777
  if (systemDatabase?.systemRole) {
634
778
  throw new Error("System Content database documents cannot be deleted");
635
779
  }
636
- const deleted = await db.transaction((tx) =>
637
- trashDocumentSubtree(
638
- tx as unknown as ReturnType<typeof getDb>,
639
- id,
640
- existing.ownerEmail as string,
641
- ),
780
+ const deleted = await trashDocumentSubtree(
781
+ db,
782
+ id,
783
+ existing.ownerEmail as string,
642
784
  );
643
785
 
644
786
  await writeAppState("refresh-signal", { ts: Date.now() });
@@ -88,6 +88,33 @@ export default defineAction({
88
88
  .where(eq(schema.documentShares.resourceId, row.database.documentId));
89
89
 
90
90
  await db.transaction(async (tx) => {
91
+ const [lockedDatabase] = await tx
92
+ .update(schema.contentDatabases)
93
+ .set({ updatedAt: sql`${schema.contentDatabases.updatedAt}` })
94
+ .where(
95
+ and(
96
+ eq(schema.contentDatabases.id, row.database.id),
97
+ eq(schema.contentDatabases.ownerEmail, row.database.ownerEmail),
98
+ isNull(schema.contentDatabases.deletedAt),
99
+ ),
100
+ )
101
+ .returning({ id: schema.contentDatabases.id });
102
+ if (!lockedDatabase) throw new Error("Database is no longer active.");
103
+ const [claimedSource] = await tx
104
+ .select({ id: schema.contentDatabaseItemKeyClaims.id })
105
+ .from(schema.contentDatabaseItemKeyClaims)
106
+ .where(
107
+ and(
108
+ eq(schema.contentDatabaseItemKeyClaims.databaseId, row.database.id),
109
+ eq(schema.contentDatabaseItemKeyClaims.documentId, row.document.id),
110
+ ),
111
+ )
112
+ .limit(1);
113
+ if (claimedSource) {
114
+ throw new Error(
115
+ "Rows with active stable-key claims cannot be duplicated.",
116
+ );
117
+ }
91
118
  await tx
92
119
  .update(schema.contentDatabaseItems)
93
120
  .set({
@@ -2,7 +2,7 @@ import { defineAction } from "@agent-native/core";
2
2
  import { writeAppState } from "@agent-native/core/application-state";
3
3
  import { getRequestUserEmail } from "@agent-native/core/server/request-context";
4
4
  import { assertAccess } from "@agent-native/core/sharing";
5
- import { and, eq, gte, inArray, sql } from "drizzle-orm";
5
+ import { and, eq, gte, inArray, isNull, sql } from "drizzle-orm";
6
6
 
7
7
  import { getDb, schema } from "../server/db/index.js";
8
8
  import { ensureDocumentsFilesMembership } from "./_content-files.js";
@@ -86,6 +86,36 @@ export default defineAction({
86
86
  }));
87
87
 
88
88
  await db.transaction(async (tx) => {
89
+ const [lockedDatabase] = await tx
90
+ .update(schema.contentDatabases)
91
+ .set({ updatedAt: sql`${schema.contentDatabases.updatedAt}` })
92
+ .where(
93
+ and(
94
+ eq(schema.contentDatabases.id, database.id),
95
+ eq(schema.contentDatabases.ownerEmail, database.ownerEmail),
96
+ isNull(schema.contentDatabases.deletedAt),
97
+ ),
98
+ )
99
+ .returning({ id: schema.contentDatabases.id });
100
+ if (!lockedDatabase) throw new Error("Database is no longer active.");
101
+ const [claimedSource] = await tx
102
+ .select({ id: schema.contentDatabaseItemKeyClaims.id })
103
+ .from(schema.contentDatabaseItemKeyClaims)
104
+ .where(
105
+ and(
106
+ eq(schema.contentDatabaseItemKeyClaims.databaseId, database.id),
107
+ inArray(
108
+ schema.contentDatabaseItemKeyClaims.documentId,
109
+ sourceDocumentIds,
110
+ ),
111
+ ),
112
+ )
113
+ .limit(1);
114
+ if (claimedSource) {
115
+ throw new Error(
116
+ "Rows with active stable-key claims cannot be duplicated.",
117
+ );
118
+ }
89
119
  await tx
90
120
  .update(schema.contentDatabaseItems)
91
121
  .set({
@@ -15,12 +15,10 @@ export default defineAction({
15
15
  run: async ({ id }) => {
16
16
  const access = await assertAccess("document", id, "admin");
17
17
  const db = getDb();
18
- const deleted = await db.transaction((tx) =>
19
- deleteTrashedDocumentSubtree(
20
- tx as unknown as ReturnType<typeof getDb>,
21
- id,
22
- access.resource.ownerEmail as string,
23
- ),
18
+ const deleted = await deleteTrashedDocumentSubtree(
19
+ db,
20
+ id,
21
+ access.resource.ownerEmail as string,
24
22
  );
25
23
  await writeAppState("refresh-signal", { ts: Date.now() });
26
24
  return { success: true, deleted: deleted.length };
@@ -145,6 +145,19 @@ export default defineAction({
145
145
  ),
146
146
  );
147
147
  }
148
+ if (removedItemIds.length > 0) {
149
+ await tx
150
+ .delete(schema.contentDatabaseItemKeyClaims)
151
+ .where(
152
+ and(
153
+ eq(schema.contentDatabaseItemKeyClaims.databaseId, database.id),
154
+ inArray(
155
+ schema.contentDatabaseItemKeyClaims.itemId,
156
+ removedItemIds,
157
+ ),
158
+ ),
159
+ );
160
+ }
148
161
  if (removedItemIds.length > 0) {
149
162
  await tx
150
163
  .delete(schema.contentDatabaseItems)
@@ -1,6 +1,6 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { assertAccess } from "@agent-native/core/sharing";
3
- import { and, eq } from "drizzle-orm";
3
+ import { and, eq, isNull, ne, sql } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
@@ -130,7 +130,65 @@ export default defineAction({
130
130
 
131
131
  const valueJson = normalizedValueJson(type, value);
132
132
  await db.transaction(async (tx) => {
133
+ const [lockedDatabase] = await tx
134
+ .update(schema.contentDatabases)
135
+ .set({ updatedAt: sql`${schema.contentDatabases.updatedAt}` })
136
+ .where(
137
+ and(
138
+ eq(schema.contentDatabases.id, database.id),
139
+ eq(schema.contentDatabases.documentId, database.documentId),
140
+ eq(schema.contentDatabases.ownerEmail, database.ownerEmail),
141
+ isNull(schema.contentDatabases.deletedAt),
142
+ ),
143
+ )
144
+ .returning({ id: schema.contentDatabases.id });
145
+ if (!lockedDatabase) throw new Error("Database is no longer active.");
146
+ const [lockedDefinition] = await tx
147
+ .update(schema.documentPropertyDefinitions)
148
+ .set({
149
+ updatedAt: sql`${schema.documentPropertyDefinitions.updatedAt}`,
150
+ })
151
+ .where(
152
+ and(
153
+ eq(schema.documentPropertyDefinitions.id, propertyId),
154
+ eq(schema.documentPropertyDefinitions.databaseId, database.id),
155
+ eq(
156
+ schema.documentPropertyDefinitions.ownerEmail,
157
+ database.ownerEmail,
158
+ ),
159
+ ),
160
+ )
161
+ .returning({
162
+ type: schema.documentPropertyDefinitions.type,
163
+ systemRole: schema.documentPropertyDefinitions.systemRole,
164
+ });
165
+ if (
166
+ !lockedDefinition ||
167
+ lockedDefinition.type !== definition.type ||
168
+ lockedDefinition.systemRole
169
+ ) {
170
+ throw new Error(
171
+ `Property "${propertyId}" changed or was deleted before its value could be written.`,
172
+ );
173
+ }
133
174
  await lockDatabaseMemberships(tx, [membership.id]);
175
+ const [conflictingClaim] = await tx
176
+ .select({ id: schema.contentDatabaseItemKeyClaims.id })
177
+ .from(schema.contentDatabaseItemKeyClaims)
178
+ .where(
179
+ and(
180
+ eq(schema.contentDatabaseItemKeyClaims.databaseId, database.id),
181
+ eq(schema.contentDatabaseItemKeyClaims.propertyId, propertyId),
182
+ eq(schema.contentDatabaseItemKeyClaims.keyValueJson, valueJson),
183
+ ne(schema.contentDatabaseItemKeyClaims.documentId, documentId),
184
+ ),
185
+ )
186
+ .limit(1);
187
+ if (conflictingClaim) {
188
+ throw new Error(
189
+ "This value is already claimed as another row's stable key.",
190
+ );
191
+ }
134
192
  const [existing] = await tx
135
193
  .select({ id: schema.documentPropertyValues.id })
136
194
  .from(schema.documentPropertyValues)
@@ -156,6 +214,16 @@ export default defineAction({
156
214
  updatedAt: now,
157
215
  });
158
216
  }
217
+ await tx
218
+ .delete(schema.contentDatabaseItemKeyClaims)
219
+ .where(
220
+ and(
221
+ eq(schema.contentDatabaseItemKeyClaims.databaseId, database.id),
222
+ eq(schema.contentDatabaseItemKeyClaims.propertyId, propertyId),
223
+ eq(schema.contentDatabaseItemKeyClaims.documentId, documentId),
224
+ ne(schema.contentDatabaseItemKeyClaims.keyValueJson, valueJson),
225
+ ),
226
+ );
159
227
  });
160
228
 
161
229
  return {