@agent-native/core-corpus 0.216.0 → 0.217.0

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 (149) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +61 -1
  3. package/corpus/templates/analytics/AGENTS.md +1 -1
  4. package/corpus/templates/analytics/CHANGELOG.md +1 -0
  5. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +9 -65
  6. package/corpus/templates/analytics/actions/list-event-catalog.ts +1 -1
  7. package/corpus/templates/analytics/actions/list-session-outputs.ts +40 -0
  8. package/corpus/templates/analytics/actions/list-session-recordings.ts +17 -4
  9. package/corpus/templates/analytics/actions/view-screen.ts +29 -3
  10. package/corpus/templates/analytics/app/i18n/en-US.ts +27 -0
  11. package/corpus/templates/analytics/app/pages/sessions/EventCatalogPage.tsx +18 -1
  12. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +3 -1
  13. package/corpus/templates/analytics/app/pages/sessions/SessionOutputs.tsx +197 -0
  14. package/corpus/templates/analytics/app/pages/sessions/SessionsTriagePage.tsx +114 -4
  15. package/corpus/templates/analytics/changelog/2026-10-15-sessions-can-find-people-who-made-a-slides-deck-but-didn-t-p.md +6 -0
  16. package/corpus/templates/analytics/changelog/2026-10-16-the-activation-funnel-counts-visitors-who-decline-analytics-.md +6 -0
  17. package/corpus/templates/analytics/server/db/schema.ts +67 -0
  18. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +9 -0
  19. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +28 -22
  20. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +36 -1
  21. package/corpus/templates/analytics/server/lib/output-outcomes.ts +712 -0
  22. package/corpus/templates/analytics/server/lib/replay-origins.ts +33 -0
  23. package/corpus/templates/analytics/server/lib/session-event-index.ts +20 -0
  24. package/corpus/templates/analytics/server/lib/session-performance.ts +6 -40
  25. package/corpus/templates/analytics/server/lib/session-recording-associations.ts +30 -1
  26. package/corpus/templates/analytics/server/lib/session-replay-triage.ts +99 -0
  27. package/corpus/templates/analytics/server/lib/session-replay.ts +45 -31
  28. package/corpus/templates/analytics/server/lib/sessions-triage-lab.ts +8 -2
  29. package/corpus/templates/analytics/server/plugins/db.ts +46 -0
  30. package/corpus/templates/analytics/shared/output-use.ts +205 -0
  31. package/corpus/templates/analytics/shared/session-events.ts +6 -0
  32. package/corpus/templates/calendar/CHANGELOG.md +6 -0
  33. package/corpus/templates/calendar/actions/create-event.ts +2 -4
  34. package/corpus/templates/calendar/actions/update-event.ts +35 -6
  35. package/corpus/templates/calendar/changelog/2026-10-21-solo-events-avoid-notification-approval.md +6 -0
  36. package/corpus/templates/community/qr/package.json +1 -1
  37. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -3
  38. package/corpus/templates/content/CHANGELOG.md +8 -0
  39. package/corpus/templates/content/actions/_browser-document-save-attempt.ts +23 -1
  40. package/corpus/templates/content/actions/_builder-cms-blob-custody.ts +467 -0
  41. package/corpus/templates/content/actions/_builder-cms-execution-lease.ts +17 -0
  42. package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +5 -0
  43. package/corpus/templates/content/actions/_builder-cms-read-client.ts +881 -21
  44. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +28 -2
  45. package/corpus/templates/content/actions/_builder-cms-write-client.ts +109 -11
  46. package/corpus/templates/content/actions/_builder-docs-client.ts +1 -0
  47. package/corpus/templates/content/actions/_builder-private-blob.test-fixture.ts +90 -0
  48. package/corpus/templates/content/actions/_builder-source-execution-guard.ts +48 -0
  49. package/corpus/templates/content/actions/_content-save-outcomes.ts +5 -0
  50. package/corpus/templates/content/actions/_database-source-utils.ts +290 -8
  51. package/corpus/templates/content/actions/_database-utils.ts +93 -7
  52. package/corpus/templates/content/actions/_database-view-setup.ts +21 -2
  53. package/corpus/templates/content/actions/_delete-content-space.ts +8 -1
  54. package/corpus/templates/content/actions/_github-source-freshness.ts +30 -4
  55. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -6
  56. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +110 -26
  57. package/corpus/templates/content/actions/change-content-database-source-role.ts +51 -7
  58. package/corpus/templates/content/actions/delete-document.ts +41 -3
  59. package/corpus/templates/content/actions/disconnect-content-database-source.ts +68 -2
  60. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +33 -2
  61. package/corpus/templates/content/actions/execute-builder-source-execution.ts +900 -140
  62. package/corpus/templates/content/actions/get-document.ts +2 -0
  63. package/corpus/templates/content/actions/permanently-delete-document.ts +6 -0
  64. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +155 -13
  65. package/corpus/templates/content/actions/prepare-builder-source-review.ts +299 -73
  66. package/corpus/templates/content/actions/set-live-body-switch.ts +45 -0
  67. package/corpus/templates/content/actions/update-document.ts +139 -20
  68. package/corpus/templates/content/actions/validate-builder-source-execution.ts +135 -26
  69. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +23 -0
  70. package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +27 -2
  71. package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +32 -8
  72. package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +27 -2
  73. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +1 -0
  74. package/corpus/templates/content/app/hooks/use-content-database.ts +40 -2
  75. package/corpus/templates/content/app/hooks/use-documents.ts +14 -0
  76. package/corpus/templates/content/app/i18n-data.ts +1 -0
  77. package/corpus/templates/content/changelog/2026-10-17-builder-content-avoids-retrying-a-write-when-builder-accepte.md +6 -0
  78. package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +1 -1
  79. package/corpus/templates/content/changelog/2026-10-20-builder-content-sync-keeps-read-and-write-status-accurate-wh.md +6 -0
  80. package/corpus/templates/content/changelog/2026-10-21-list-rows-can-show-each-pages-first-lines.md +6 -0
  81. package/corpus/templates/content/changelog/2026-10-22-a-collection-linked-to-a-big-github-folder-fills-faster-and-p.md +6 -0
  82. package/corpus/templates/content/docs/product/capabilities/content.source.adapters.md +29 -0
  83. package/corpus/templates/content/parity/matrix.md +42 -42
  84. package/corpus/templates/content/parity/matrix.ts +3 -1
  85. package/corpus/templates/content/server/db/schema.ts +20 -0
  86. package/corpus/templates/content/server/lib/content-trash-purge.ts +15 -1
  87. package/corpus/templates/content/server/lib/live-body-writer.ts +138 -26
  88. package/corpus/templates/content/server/live-copy/binding.ts +31 -0
  89. package/corpus/templates/content/server/live-copy/bounded-task.d.ts +6 -0
  90. package/corpus/templates/content/server/live-copy/exec-db.ts +1 -0
  91. package/corpus/templates/content/server/live-copy/holds.ts +59 -0
  92. package/corpus/templates/content/server/live-copy/persist.ts +656 -0
  93. package/corpus/templates/content/server/live-copy/switch.ts +136 -0
  94. package/corpus/templates/content/server/live-copy/task-names.ts +4 -0
  95. package/corpus/templates/content/server/live-copy/tasks.ts +183 -0
  96. package/corpus/templates/content/server/live-copy/write-cause.ts +85 -0
  97. package/corpus/templates/content/server/live-copy/write.ts +85 -0
  98. package/corpus/templates/content/server/plugins/builder-private-payload-cleanup.ts +22 -0
  99. package/corpus/templates/content/server/plugins/collab.ts +3 -0
  100. package/corpus/templates/content/server/plugins/db.ts +28 -0
  101. package/corpus/templates/content/shared/api.ts +30 -1
  102. package/corpus/templates/content/shared/database-item-layout.ts +54 -22
  103. package/corpus/templates/content/shared/document-intent-merge.ts +16 -9
  104. package/corpus/templates/content/shared/og-description.ts +258 -36
  105. package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +5 -1
  106. package/corpus/templates/design/.generated/bridge/shader-runtime.generated.ts +25 -4
  107. package/corpus/templates/design/CHANGELOG.md +2 -0
  108. package/corpus/templates/design/actions/get-getting-started.ts +53 -0
  109. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +10 -0
  110. package/corpus/templates/design/app/components/design/bridge/native-shader-runtime.bridge.ts +132 -79
  111. package/corpus/templates/design/app/components/design/bridge/native-source-provider.ts +268 -130
  112. package/corpus/templates/design/app/components/design/bridge/native-worker-scene-host.ts +122 -42
  113. package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +41 -4
  114. package/corpus/templates/design/app/components/design/design-canvas/text-content-change-result.ts +46 -12
  115. package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +15 -8
  116. package/corpus/templates/design/app/components/layout/GettingStarted.tsx +50 -0
  117. package/corpus/templates/design/app/components/layout/Sidebar.tsx +3 -0
  118. package/corpus/templates/design/app/components/templates/DesignTemplateGrid.tsx +163 -0
  119. package/corpus/templates/design/app/components/templates/DesignTemplateLibrary.tsx +65 -78
  120. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +6 -2
  121. package/corpus/templates/design/app/global.css +6 -0
  122. package/corpus/templates/design/app/i18n/en-US.ts +17 -0
  123. package/corpus/templates/design/app/lib/canvas-load-outcome.ts +147 -0
  124. package/corpus/templates/design/app/lib/canvas-outcome-tracking.ts +531 -0
  125. package/corpus/templates/design/app/lib/canvas-style-kind.ts +50 -0
  126. package/corpus/templates/design/app/pages/Index.tsx +10 -3
  127. package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +86 -29
  128. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +38 -3
  129. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-core.ts +9 -0
  130. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-files-and-saving.ts +2 -0
  131. package/corpus/templates/design/changelog/2026-10-21-a-getting-started-checklist-at-the-bottom-of-the-sidebar-tra.md +6 -0
  132. package/corpus/templates/design/changelog/2026-10-21-templates-now-open-a-read-only-preview-from-the-card-and-the.md +6 -0
  133. package/corpus/templates/design/changelog/2026-10-22-collapsed-shader-layers-no-longer-interrupt-other-shaders-on.md +6 -0
  134. package/corpus/templates/design/changelog/2026-10-22-shader-fills-keep-rendering-when-other-screen-content-cannot.md +6 -0
  135. package/corpus/templates/design/scripts/design-export-validation/export-baseline.json +24 -0
  136. package/corpus/templates/design/server/lib/getting-started.ts +267 -0
  137. package/corpus/templates/design/server/plugins/getting-started.ts +5 -0
  138. package/corpus/templates/design/shared/design-template-presets/analytics-dashboard.ts +127 -0
  139. package/corpus/templates/design/shared/design-template-presets/design-studio.ts +98 -0
  140. package/corpus/templates/design/shared/design-template-presets/messaging-app.ts +101 -0
  141. package/corpus/templates/design/shared/design-template-presets/project-tasks.ts +120 -0
  142. package/corpus/templates/design/shared/design-template-presets.ts +8 -0
  143. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
  144. package/corpus/templates/slides/CHANGELOG.md +6 -0
  145. package/corpus/templates/slides/actions/delete-deck.ts +7 -0
  146. package/corpus/templates/slides/changelog/2026-10-21-connected-deck-deletion-requires-approval.md +6 -0
  147. package/corpus/templates/slides/scripts/edit-fidelity/client-action.ts +61 -0
  148. package/corpus/templates/slides/scripts/edit-fidelity/run.ts +10 -0
  149. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
+ import { fail, isActionContractError } from "@agent-native/core/action";
3
4
  import { and, asc, eq, inArray, isNull, lt, lte, or, sql } from "drizzle-orm";
4
5
 
5
6
  import { getDb, schema } from "../server/db/index.js";
@@ -71,8 +72,16 @@ import {
71
72
  localFolderSourceIdentityFromMetadata,
72
73
  } from "./_local-folder-source.js";
73
74
  export { bulkChunkSizeForColumnCount } from "./_batch-utils.js";
75
+ import {
76
+ builderExecutionPayloadReference,
77
+ isBuilderPrivatePayloadBoundToSource,
78
+ cleanupBuilderPrivatePayload,
79
+ enqueueBuilderPrivatePayloadCleanup,
80
+ readBuilderExecutionPayload,
81
+ } from "./_builder-cms-blob-custody.js";
74
82
  import {
75
83
  BuilderCmsContentEntryReadError,
84
+ RETRYABLE_BUILDER_SOURCE_BINDING_ERRORS,
76
85
  readBuilderCmsContentEntryResult,
77
86
  readBuilderCmsContentEntries,
78
87
  readBuilderCmsModelFields,
@@ -97,6 +106,7 @@ import {
97
106
  type ExistingBuilderSourceRowIdentity,
98
107
  } from "./_builder-cms-source-adapter.js";
99
108
  import { mergeBuilderCmsWriteSettingsIntoJson } from "./_builder-cms-write-settings.js";
109
+ import { assertNoActiveBuilderSourceExecutions } from "./_builder-source-execution-guard.js";
100
110
  import { lockContentDatabaseMutation } from "./_content-database-mutation-lock.js";
101
111
  import { ensureDocumentsFilesMembership } from "./_content-files.js";
102
112
  import {
@@ -192,6 +202,7 @@ type SourceMetadataRecord = {
192
202
  readMode?: string | null;
193
203
  connectionId?: string | null;
194
204
  connectionLabel?: string | null;
205
+ builderSpacePublicKey?: string | null;
195
206
  truthPolicy?: ContentDatabaseSource["metadata"]["truthPolicy"];
196
207
  syncPolicy?: "manual" | "keep_in_sync";
197
208
  liveBridgeEnabled?: boolean;
@@ -201,9 +212,11 @@ type SourceMetadataRecord = {
201
212
  lastReadMatchedRowCount?: number;
202
213
  lastReadLimit?: number;
203
214
  lastReadFetchedEntryCount?: number;
215
+ lastReadCollectionCount?: number;
204
216
  lastReadPartial?: boolean;
205
217
  lastReadHasMore?: boolean;
206
218
  lastReadNextOffset?: number;
219
+ lastReadNextCursor?: string;
207
220
  lastReadSuspiciousEmpty?: boolean;
208
221
  activeReadSourceRowIds?: string[];
209
222
  sourceFetchState?: "idle" | "fetching" | "error";
@@ -757,7 +770,7 @@ function serializeReviewEvent(
757
770
  }
758
771
 
759
772
  function serializeExecution(
760
- row: ContentDatabaseSourceExecutionRowDb,
773
+ row: ContentDatabaseSourceExecutionRowDb & { payloadUnavailable?: true },
761
774
  ): ContentDatabaseSourceExecution {
762
775
  return {
763
776
  id: row.id,
@@ -767,7 +780,10 @@ function serializeExecution(
767
780
  state: normalizeExecutionState(row.state),
768
781
  idempotencyKey: row.idempotencyKey,
769
782
  summary: row.summary,
770
- payload: parseObject<Record<string, unknown>>(row.payloadJson) ?? {},
783
+ payload: row.payloadUnavailable
784
+ ? { unavailable: true }
785
+ : (parseObject<Record<string, unknown>>(row.payloadJson) ?? {}),
786
+ ...(row.payloadUnavailable ? { payloadUnavailable: true } : {}),
771
787
  lastError: row.lastError,
772
788
  createdAt: row.createdAt,
773
789
  updatedAt: row.updatedAt,
@@ -994,7 +1010,7 @@ class BuilderBodyHydrationError extends Error {
994
1010
  }
995
1011
  }
996
1012
 
997
- function builderBodyHydrationFailureEvidence(error: unknown) {
1013
+ export function builderBodyHydrationFailureEvidence(error: unknown) {
998
1014
  if (
999
1015
  error instanceof BuilderCmsContentEntryReadError ||
1000
1016
  error instanceof BuilderBodyHydrationError
@@ -1006,6 +1022,24 @@ function builderBodyHydrationFailureEvidence(error: unknown) {
1006
1022
  message: error.message,
1007
1023
  } as const;
1008
1024
  }
1025
+ if (isActionContractError(error)) {
1026
+ return {
1027
+ reason:
1028
+ error.statusCode === 404
1029
+ ? ("not_found" as const)
1030
+ : error.statusCode === 400 ||
1031
+ error.statusCode === 409 ||
1032
+ error.statusCode === 412
1033
+ ? ("access_denied" as const)
1034
+ : ("transient_read_failure" as const),
1035
+ providerStatus: error.errorCode,
1036
+ retryable:
1037
+ RETRYABLE_BUILDER_SOURCE_BINDING_ERRORS.has(error.errorCode) ||
1038
+ error.statusCode === 424 ||
1039
+ error.statusCode >= 500,
1040
+ message: error.message,
1041
+ };
1042
+ }
1009
1043
  return {
1010
1044
  reason: "conversion_failed" as const,
1011
1045
  providerStatus: "local_conversion",
@@ -1400,6 +1434,7 @@ type BuilderLiveBodyReadResult =
1400
1434
  entry: null;
1401
1435
  providerStatus:
1402
1436
  | "http_404"
1437
+ | "http_200_not_found"
1403
1438
  | "http_200_unexpected_entry"
1404
1439
  | "mcp_not_found";
1405
1440
  };
@@ -1411,6 +1446,8 @@ async function readBuilderEntryWithLiveBodyFromSourceRow(args: {
1411
1446
  >;
1412
1447
  sourceTable: string;
1413
1448
  fallbackTitle: string;
1449
+ expectedSourceSpace?: string | null;
1450
+ expectedSourceConnectionId?: string | null;
1414
1451
  }): Promise<BuilderLiveBodyReadResult> {
1415
1452
  const sourceValues =
1416
1453
  parseObject<Record<string, DocumentPropertyValue>>(
@@ -1420,6 +1457,9 @@ async function readBuilderEntryWithLiveBodyFromSourceRow(args: {
1420
1457
  model: args.sourceTable,
1421
1458
  entryId: args.row.sourceRowId,
1422
1459
  strictEntryIdentity: true,
1460
+ expectedSourceSpace: args.expectedSourceSpace,
1461
+ expectedSourceConnectionId: args.expectedSourceConnectionId,
1462
+ requireBoundOAuth: true,
1423
1463
  });
1424
1464
  if (liveRead.state === "not_found") return liveRead;
1425
1465
  const liveEntry = liveRead.entry;
@@ -1967,6 +2007,8 @@ async function processBuilderBodyHydrationJob(
1967
2007
  documentContent?: string | null;
1968
2008
  bodyHydrationVersion?: string | null;
1969
2009
  bodyEntry?: BuilderCmsSourceEntry | null;
2010
+ expectedSourceSpace?: string | null;
2011
+ expectedSourceConnectionId?: string | null;
1970
2012
  },
1971
2013
  ) {
1972
2014
  const db = getDb();
@@ -2037,6 +2079,8 @@ async function processBuilderBodyHydrationJob(
2037
2079
  row: sourceRow,
2038
2080
  sourceTable: row.sourceTable,
2039
2081
  fallbackTitle: entry.title,
2082
+ expectedSourceSpace: preloaded?.expectedSourceSpace,
2083
+ expectedSourceConnectionId: preloaded?.expectedSourceConnectionId,
2040
2084
  });
2041
2085
  if (liveRead.state === "not_found") {
2042
2086
  throw new BuilderBodyHydrationError(
@@ -2144,6 +2188,8 @@ async function processBuilderBodyHydrationJob(
2144
2188
  row: sourceRow,
2145
2189
  sourceTable: row.sourceTable,
2146
2190
  fallbackTitle: entry.title,
2191
+ expectedSourceSpace: preloaded?.expectedSourceSpace,
2192
+ expectedSourceConnectionId: preloaded?.expectedSourceConnectionId,
2147
2193
  });
2148
2194
  if (liveRead.state === "body") {
2149
2195
  const liveEntry = liveRead.entry;
@@ -2887,6 +2933,13 @@ export async function processBuilderBodyHydrationQueue(args: {
2887
2933
  retryFailed?: boolean;
2888
2934
  }) {
2889
2935
  const db = getDb();
2936
+ const [hydrationSource] = await db
2937
+ .select({ metadataJson: schema.contentDatabaseSources.metadataJson })
2938
+ .from(schema.contentDatabaseSources)
2939
+ .where(eq(schema.contentDatabaseSources.id, args.sourceId));
2940
+ const hydrationSourceMetadata = hydrationSource
2941
+ ? (parseObject<SourceMetadataRecord>(hydrationSource.metadataJson) ?? {})
2942
+ : {};
2890
2943
  const limit = normalizeHydrationLimit(args.limit);
2891
2944
  const now = new Date().toISOString();
2892
2945
  if (args.retryFailed) {
@@ -3130,6 +3183,9 @@ export async function processBuilderBodyHydrationQueue(args: {
3130
3183
  model,
3131
3184
  includeBodies: true,
3132
3185
  limit: 10_000,
3186
+ expectedSourceSpace: hydrationSourceMetadata.builderSpacePublicKey,
3187
+ expectedSourceConnectionId: hydrationSourceMetadata.connectionId,
3188
+ requireBoundOAuth: true,
3133
3189
  }),
3134
3190
  ),
3135
3191
  );
@@ -3182,6 +3238,8 @@ export async function processBuilderBodyHydrationQueue(args: {
3182
3238
  bodyHydrationVersion:
3183
3239
  bodyHydrationVersionByItemId.get(job.databaseItemId) ?? null,
3184
3240
  bodyEntry: bodyEntryById.get(job.sourceRowId) ?? null,
3241
+ expectedSourceSpace: hydrationSourceMetadata.builderSpacePublicKey,
3242
+ expectedSourceConnectionId: hydrationSourceMetadata.connectionId,
3185
3243
  documentContent: documentContentById.has(job.documentId)
3186
3244
  ? (documentContentById.get(job.documentId) ?? null)
3187
3245
  : undefined,
@@ -4023,6 +4081,7 @@ export async function getContentDatabaseSourceSnapshotForWrite(
4023
4081
  ? loadSourceSnapshot(source, database, {
4024
4082
  includeHeavyBuilderBodyValues: true,
4025
4083
  documentIds,
4084
+ hydrateExecutionPayloads: documentIds !== undefined,
4026
4085
  })
4027
4086
  : null;
4028
4087
  }
@@ -4038,6 +4097,7 @@ export async function getContentDatabaseSourceSnapshotForWrite(
4038
4097
  ? loadSourceSnapshot(source, database, {
4039
4098
  includeHeavyBuilderBodyValues: true,
4040
4099
  documentIds,
4100
+ hydrateExecutionPayloads: documentIds !== undefined,
4041
4101
  })
4042
4102
  : null;
4043
4103
  }
@@ -4183,6 +4243,9 @@ export async function getContentDatabaseSourceSnapshotForReview(
4183
4243
  if (reviewableChanges.some((changeSet) => !changeSet.documentId)) {
4184
4244
  return loadSourceSnapshot(source, database, {
4185
4245
  includeHeavyBuilderBodyValues: true,
4246
+ hydrateExecutionPayloadsForChangeSetIds: reviewableChanges.map(
4247
+ (changeSet) => changeSet.id,
4248
+ ),
4186
4249
  });
4187
4250
  }
4188
4251
  const candidateDocumentIds = new Set(
@@ -4451,10 +4514,102 @@ async function loadSourceSnapshotRowsOptimistically(args: {
4451
4514
  };
4452
4515
  }
4453
4516
 
4517
+ async function hydrateSnapshotExecutionRow(
4518
+ row: ContentDatabaseSourceExecutionRowDb,
4519
+ ): Promise<ContentDatabaseSourceExecutionRowDb> {
4520
+ const read = (current: ContentDatabaseSourceExecutionRowDb) =>
4521
+ readBuilderExecutionPayload({
4522
+ payloadJson: current.payloadJson,
4523
+ binding: {
4524
+ ownerEmail: current.ownerEmail,
4525
+ sourceId: current.sourceId,
4526
+ changeSetId: current.changeSetId,
4527
+ executionId: current.id,
4528
+ idempotencyKey: current.idempotencyKey,
4529
+ },
4530
+ });
4531
+ try {
4532
+ return { ...row, payloadJson: JSON.stringify(await read(row)) };
4533
+ } catch (error) {
4534
+ // Each execution state change stores a new payload blob and deletes the
4535
+ // previous one after commit, so a row read just before that commit can
4536
+ // name a deleted blob. Only a row that has since moved may be re-read.
4537
+ const [current] = await getDb()
4538
+ .select()
4539
+ .from(schema.contentDatabaseSourceExecutions)
4540
+ .where(eq(schema.contentDatabaseSourceExecutions.id, row.id))
4541
+ .limit(1);
4542
+ if (!current || current.payloadJson === row.payloadJson) throw error;
4543
+ return { ...current, payloadJson: JSON.stringify(await read(current)) };
4544
+ }
4545
+ }
4546
+
4547
+ const SNAPSHOT_EXECUTION_HYDRATION_CONCURRENCY = 4;
4548
+
4549
+ export function latestSnapshotExecutionIds(
4550
+ rows: Array<{ id: string; changeSetId: string }>,
4551
+ ) {
4552
+ const latestIds = new Map<string, string>();
4553
+ for (const row of rows) latestIds.set(row.changeSetId, row.id);
4554
+ return new Set(latestIds.values());
4555
+ }
4556
+
4557
+ export async function hydrateSnapshotExecutionRows(
4558
+ rows: ContentDatabaseSourceExecutionRowDb[],
4559
+ hydrate: (
4560
+ row: ContentDatabaseSourceExecutionRowDb,
4561
+ ) => Promise<ContentDatabaseSourceExecutionRowDb> = hydrateSnapshotExecutionRow,
4562
+ shouldHydrate: (row: ContentDatabaseSourceExecutionRowDb) => boolean = () =>
4563
+ true,
4564
+ ): Promise<
4565
+ Array<ContentDatabaseSourceExecutionRowDb & { payloadUnavailable?: true }>
4566
+ > {
4567
+ const hydrated = new Array<
4568
+ ContentDatabaseSourceExecutionRowDb & { payloadUnavailable?: true }
4569
+ >(rows.length);
4570
+ let nextIndex = 0;
4571
+ const workerCount = Math.min(
4572
+ SNAPSHOT_EXECUTION_HYDRATION_CONCURRENCY,
4573
+ rows.length,
4574
+ );
4575
+ await Promise.all(
4576
+ Array.from({ length: workerCount }, async () => {
4577
+ while (nextIndex < rows.length) {
4578
+ const index = nextIndex++;
4579
+ const row = rows[index];
4580
+ if (!row) continue;
4581
+ if (!shouldHydrate(row)) {
4582
+ hydrated[index] = {
4583
+ ...row,
4584
+ payloadJson: JSON.stringify({ unavailable: true }),
4585
+ payloadUnavailable: true,
4586
+ };
4587
+ continue;
4588
+ }
4589
+ try {
4590
+ hydrated[index] = await hydrate(row);
4591
+ } catch {
4592
+ hydrated[index] = {
4593
+ ...row,
4594
+ payloadJson: JSON.stringify({ unavailable: true }),
4595
+ payloadUnavailable: true,
4596
+ };
4597
+ }
4598
+ }
4599
+ }),
4600
+ );
4601
+ return hydrated;
4602
+ }
4603
+
4454
4604
  async function loadSourceSnapshot(
4455
4605
  source: ContentDatabaseSourceRowDb,
4456
4606
  database: ContentDatabaseRow | ContentDatabase,
4457
- options: { includeHeavyBuilderBodyValues: boolean; documentIds?: string[] },
4607
+ options: {
4608
+ includeHeavyBuilderBodyValues: boolean;
4609
+ documentIds?: string[];
4610
+ hydrateExecutionPayloads?: boolean;
4611
+ hydrateExecutionPayloadsForChangeSetIds?: string[];
4612
+ },
4458
4613
  ): Promise<ContentDatabaseSource> {
4459
4614
  const db = getDb();
4460
4615
  const [fieldRows, changeRows, propertyDefs] = await Promise.all([
@@ -4566,7 +4721,27 @@ async function loadSourceSnapshot(
4566
4721
  string,
4567
4722
  ContentDatabaseSourceExecution[]
4568
4723
  >();
4569
- for (const row of executionRows) {
4724
+ const hydrateAllExecutionPayloads = options.hydrateExecutionPayloads === true;
4725
+ const hydrateChangeSetIds =
4726
+ options.hydrateExecutionPayloadsForChangeSetIds !== undefined
4727
+ ? new Set(options.hydrateExecutionPayloadsForChangeSetIds)
4728
+ : null;
4729
+ const executionIdsToHydrate = latestSnapshotExecutionIds(executionRows);
4730
+ const hydratedExecutionRows =
4731
+ hydrateAllExecutionPayloads || hydrateChangeSetIds !== null
4732
+ ? await hydrateSnapshotExecutionRows(
4733
+ executionRows,
4734
+ undefined,
4735
+ (row) =>
4736
+ executionIdsToHydrate.has(row.id) &&
4737
+ (hydrateAllExecutionPayloads ||
4738
+ hydrateChangeSetIds?.has(row.changeSetId) === true),
4739
+ )
4740
+ : executionRows.map((row) => ({
4741
+ ...row,
4742
+ payloadUnavailable: true as const,
4743
+ }));
4744
+ for (const row of hydratedExecutionRows) {
4570
4745
  const executions = executionsByChangeSetId.get(row.changeSetId) ?? [];
4571
4746
  executions.push(serializeExecution(row));
4572
4747
  executionsByChangeSetId.set(row.changeSetId, executions);
@@ -4888,6 +5063,7 @@ async function loadSourceSnapshot(
4888
5063
  readMode: metadata.readMode ?? null,
4889
5064
  connectionId: metadata.connectionId ?? null,
4890
5065
  connectionLabel: metadata.connectionLabel ?? null,
5066
+ builderSpacePublicKey: metadata.builderSpacePublicKey ?? null,
4891
5067
  truthPolicy:
4892
5068
  metadata.truthPolicy === "database_primary" ||
4893
5069
  metadata.truthPolicy === "source_primary" ||
@@ -4922,6 +5098,10 @@ async function loadSourceSnapshot(
4922
5098
  typeof metadata.lastReadFetchedEntryCount === "number"
4923
5099
  ? metadata.lastReadFetchedEntryCount
4924
5100
  : undefined,
5101
+ lastReadCollectionCount:
5102
+ typeof metadata.lastReadCollectionCount === "number"
5103
+ ? metadata.lastReadCollectionCount
5104
+ : undefined,
4925
5105
  lastReadPartial:
4926
5106
  typeof metadata.lastReadPartial === "boolean"
4927
5107
  ? metadata.lastReadPartial
@@ -5134,15 +5314,22 @@ export function serializeBuilderCmsSourceReadMetadataRecord(args: {
5134
5314
  ...existingMetadata,
5135
5315
  builderModelFields:
5136
5316
  args.builderModelFields ?? existingMetadata?.builderModelFields,
5317
+ builderSpacePublicKey:
5318
+ args.progress?.sourceSpacePublicKey ??
5319
+ existingMetadata?.builderSpacePublicKey,
5320
+ connectionId:
5321
+ args.progress?.sourceConnectionId ?? existingMetadata?.connectionId,
5137
5322
  readMode: args.readState === "live" ? "builder-api" : "fixture",
5138
5323
  liveReadConfigured: args.readState === "live",
5139
5324
  lastReadEntryCount: args.entryCount,
5140
5325
  lastReadMatchedRowCount: args.matchedRowCount,
5141
5326
  lastReadLimit: args.progress?.requestedLimit,
5142
5327
  lastReadFetchedEntryCount: args.progress?.fetchedEntryCount,
5328
+ lastReadCollectionCount: args.progress?.observedCollectionCount,
5143
5329
  lastReadPartial: args.progress?.partial,
5144
5330
  lastReadHasMore: args.progress?.hasMore,
5145
5331
  lastReadNextOffset: args.progress?.nextOffset,
5332
+ lastReadNextCursor: args.progress?.nextCursor,
5146
5333
  lastReadSuspiciousEmpty: args.suspiciousEmpty === true,
5147
5334
  activeReadSourceRowIds: args.suspiciousEmpty
5148
5335
  ? []
@@ -5167,6 +5354,31 @@ export function serializeBuilderCmsSourceReadMetadataRecord(args: {
5167
5354
  return JSON.stringify(metadata);
5168
5355
  }
5169
5356
 
5357
+ export function assertBuilderCmsContinuationIdentity(args: {
5358
+ continueOffset: number;
5359
+ activeReadSourceRowIds: string[];
5360
+ entries?: Array<{ id: string }>;
5361
+ }) {
5362
+ if (args.continueOffset <= 0) return;
5363
+ if (
5364
+ new Set(args.activeReadSourceRowIds).size !==
5365
+ args.activeReadSourceRowIds.length ||
5366
+ args.activeReadSourceRowIds.length > args.continueOffset
5367
+ ) {
5368
+ fail(
5369
+ "Builder source continuation identity exceeds its saved provider offset. Start a full refresh before mutating the snapshot.",
5370
+ { errorCode: "builder_source_continuation_mismatch", statusCode: 409 },
5371
+ );
5372
+ }
5373
+ const activeIds = new Set(args.activeReadSourceRowIds);
5374
+ if (args.entries?.some((entry) => activeIds.has(entry.id))) {
5375
+ fail(
5376
+ "Builder source continuation repeated an entry from an earlier page. Start a full refresh before mutating the snapshot.",
5377
+ { errorCode: "builder_source_continuation_mismatch", statusCode: 409 },
5378
+ );
5379
+ }
5380
+ }
5381
+
5170
5382
  export function serializeSourceCapabilitiesRecord(
5171
5383
  overrides: Partial<ContentDatabaseSourceCapabilities> = {},
5172
5384
  ) {
@@ -6183,9 +6395,60 @@ async function deleteSourceChangeSetRecords(args: {
6183
6395
  )
6184
6396
  : eq(schema.contentDatabaseSourceChangeSets.sourceId, args.sourceId);
6185
6397
 
6186
- await db.delete(schema.contentDatabaseSourceExecutions).where(executionWhere);
6187
- await db.delete(schema.contentDatabaseSourceChangeReviews).where(reviewWhere);
6188
- await db.delete(schema.contentDatabaseSourceChangeSets).where(changeSetWhere);
6398
+ const references = new Set<string>();
6399
+ await db.transaction(async (tx) => {
6400
+ const [source] = await tx
6401
+ .select({ databaseId: schema.contentDatabaseSources.databaseId })
6402
+ .from(schema.contentDatabaseSources)
6403
+ .where(eq(schema.contentDatabaseSources.id, args.sourceId));
6404
+ if (!source) return;
6405
+ await lockContentDatabaseMutation(
6406
+ tx as unknown as ReturnType<typeof getDb>,
6407
+ source.databaseId,
6408
+ );
6409
+ await assertNoActiveBuilderSourceExecutions(
6410
+ tx as unknown as ReturnType<typeof getDb>,
6411
+ [args.sourceId],
6412
+ args.changeSetIds,
6413
+ );
6414
+ const deletedExecutions = await tx
6415
+ .delete(schema.contentDatabaseSourceExecutions)
6416
+ .where(executionWhere)
6417
+ .returning({
6418
+ ownerEmail: schema.contentDatabaseSourceExecutions.ownerEmail,
6419
+ sourceId: schema.contentDatabaseSourceExecutions.sourceId,
6420
+ payloadJson: schema.contentDatabaseSourceExecutions.payloadJson,
6421
+ });
6422
+ for (const row of deletedExecutions) {
6423
+ const reference = builderExecutionPayloadReference(row.payloadJson);
6424
+ if (
6425
+ reference &&
6426
+ isBuilderPrivatePayloadBoundToSource(
6427
+ reference,
6428
+ row.ownerEmail,
6429
+ row.sourceId,
6430
+ )
6431
+ ) {
6432
+ references.add(reference);
6433
+ await enqueueBuilderPrivatePayloadCleanup(
6434
+ tx as unknown as ReturnType<typeof getDb>,
6435
+ reference,
6436
+ );
6437
+ }
6438
+ }
6439
+ await tx
6440
+ .delete(schema.contentDatabaseSourceChangeReviews)
6441
+ .where(reviewWhere);
6442
+ await tx
6443
+ .delete(schema.contentDatabaseSourceChangeSets)
6444
+ .where(changeSetWhere);
6445
+ });
6446
+ for (const reference of references) {
6447
+ await cleanupBuilderPrivatePayload(
6448
+ reference,
6449
+ "deleted source execution payload",
6450
+ );
6451
+ }
6189
6452
  }
6190
6453
 
6191
6454
  async function pruneDuplicateOpenSourceChangeSets(sourceId: string) {
@@ -6850,6 +7113,8 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
6850
7113
  try {
6851
7114
  builderModelFields = await readBuilderCmsModelFields({
6852
7115
  model: args.source.sourceTable,
7116
+ expectedSourceSpace: sourceMetadata.builderSpacePublicKey,
7117
+ expectedSourceConnectionId: sourceMetadata.connectionId,
6853
7118
  });
6854
7119
  builderModelFields = mergeBuilderCmsModelFieldsPreservingReferenceModels({
6855
7120
  existing: sourceMetadata.builderModelFields,
@@ -6873,6 +7138,8 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
6873
7138
  .where(eq(schema.contentDatabaseSourceRows.sourceId, args.source.id));
6874
7139
  const builderRead = await readBuilderCmsContentEntries({
6875
7140
  model: args.source.sourceTable,
7141
+ expectedSourceSpace: sourceMetadata.builderSpacePublicKey,
7142
+ expectedSourceConnectionId: sourceMetadata.connectionId,
6876
7143
  fieldPaths: [
6877
7144
  ...existingFields.map((field) => field.sourceFieldKey),
6878
7145
  ...projectionModelFields.map((field) => `data.${field.name}`),
@@ -6882,6 +7149,16 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
6882
7149
  : BUILDER_CMS_REFRESH_INITIAL_PAGES,
6883
7150
  limit: args.runFullRefresh ? 10_000 : undefined,
6884
7151
  offset: continueOffset,
7152
+ afterId:
7153
+ continueOffset > 0 &&
7154
+ typeof sourceMetadata.lastReadNextCursor === "string"
7155
+ ? sourceMetadata.lastReadNextCursor
7156
+ : undefined,
7157
+ requireBoundOAuth: true,
7158
+ startingFetchedEntryCount:
7159
+ continueOffset > 0 ? activeReadSourceRowIds.length : 0,
7160
+ expectedCollectionCount:
7161
+ continueOffset > 0 ? sourceMetadata.lastReadCollectionCount : undefined,
6885
7162
  });
6886
7163
  if (builderRead.state === "error") {
6887
7164
  throw new Error(
@@ -6895,6 +7172,11 @@ export async function resyncBuilderCmsSourceSnapshot(args: {
6895
7172
  (builderRead.progress?.startOffset ?? 0) > 0);
6896
7173
  const builderEntries =
6897
7174
  builderRead.state === "live" ? builderRead.entries : [];
7175
+ assertBuilderCmsContinuationIdentity({
7176
+ continueOffset,
7177
+ activeReadSourceRowIds,
7178
+ entries: builderEntries,
7179
+ });
6898
7180
  if (
6899
7181
  args.refreshClaimId &&
6900
7182
  !(await renewBuilderCmsSourceRefreshClaim({
@@ -42,10 +42,15 @@ import {
42
42
  type ContentSidebarViewOrder,
43
43
  type DocumentProperty,
44
44
  } from "../shared/api.js";
45
+ import { contentDatabaseViewsShowPageText } from "../shared/database-item-layout.js";
45
46
  import {
46
47
  applyContentDatabaseTableQuery,
47
48
  contentDatabaseTableQueryUsesProperties,
48
49
  } from "../shared/database-query.js";
50
+ import {
51
+ buildListRowExcerpt,
52
+ EXCERPT_SOURCE_CHARS,
53
+ } from "../shared/og-description.js";
49
54
  import {
50
55
  evaluatePropertyFormula,
51
56
  isBlocksPropertyType,
@@ -55,6 +60,12 @@ import {
55
60
  type DocumentPropertyType,
56
61
  } from "../shared/properties.js";
57
62
  import { deleteBlocksFieldIdentity } from "./_blocks-field-identity.js";
63
+ import {
64
+ builderExecutionPayloadReference,
65
+ cleanupBuilderPrivatePayload,
66
+ enqueueBuilderPrivatePayloadCleanup,
67
+ isBuilderPrivatePayloadBoundToSource,
68
+ } from "./_builder-cms-blob-custody.js";
58
69
  import { favoriteDocumentIds } from "./_content-favorites.js";
59
70
  import {
60
71
  listContentOrganizationMemberships,
@@ -82,6 +93,7 @@ import {
82
93
  import {
83
94
  listPropertiesForDatabaseDocuments,
84
95
  listPropertiesForDatabase,
96
+ parseDatabaseViewConfig,
85
97
  readDatabasePropertySchema,
86
98
  serializeDatabase,
87
99
  type DatabasePropertySchema,
@@ -228,6 +240,7 @@ type DocumentListRow = Omit<
228
240
  | "content"
229
241
  | "collabBodyRevision"
230
242
  | "liveBodySwitchedAt"
243
+ | "liveCopyVersion"
231
244
  | "createdBy"
232
245
  | "updatedBy"
233
246
  | "creationRequestDigest"
@@ -1009,6 +1022,13 @@ export async function getContentDatabasePageResponse(
1009
1022
  });
1010
1023
  }
1011
1024
 
1025
+ // List rows draw from paged reads. Unpaged reads come from menus and pickers
1026
+ // that never show page text, so they skip the body reads.
1027
+ const includeExcerpts =
1028
+ limit !== null &&
1029
+ contentDatabaseViewsShowPageText(
1030
+ parseDatabaseViewConfig(database.viewConfigJson).views,
1031
+ );
1012
1032
  const documents =
1013
1033
  items.length > 0
1014
1034
  ? await db
@@ -1236,12 +1256,50 @@ export async function getContentDatabasePageResponse(
1236
1256
  const serializedDocumentIds = new Set(
1237
1257
  serializedItems.map((item) => item.document.id),
1238
1258
  );
1239
- const sourceSnapshots =
1259
+ // Reading a collection doesn't open every row's page, so excerpts come only
1260
+ // from pages the reader can view.
1261
+ const excerptDocumentIds = includeExcerpts
1262
+ ? serializedItems
1263
+ .filter((item) => item.document.canView)
1264
+ .map((item) => item.document.id)
1265
+ : [];
1266
+ const [sourceSnapshots, bodyPrefixByDocumentId] = await Promise.all([
1240
1267
  options.includeSources === false
1241
1268
  ? []
1242
- : await getAllContentDatabaseSourceSnapshots(database, {
1269
+ : getAllContentDatabaseSourceSnapshots(database, {
1243
1270
  documentIds: limit !== null ? [...serializedDocumentIds] : undefined,
1244
- });
1271
+ }),
1272
+ // Only the start of each returned page's body, and only when a view
1273
+ // shows it. Filters and sorts that run here have already cut the page.
1274
+ // The read takes one character more to tell a longer body from one that
1275
+ // fits.
1276
+ excerptDocumentIds.length > 0
1277
+ ? db
1278
+ .select({
1279
+ id: schema.documents.id,
1280
+ bodyPrefix: sql<
1281
+ string | null
1282
+ >`substr(${schema.documents.content}, 1, ${sql.raw(String(EXCERPT_SOURCE_CHARS + 1))})`,
1283
+ })
1284
+ .from(schema.documents)
1285
+ .where(inArray(schema.documents.id, excerptDocumentIds))
1286
+ .then(
1287
+ (rows) =>
1288
+ new Map(rows.map((row) => [row.id, row.bodyPrefix ?? ""])),
1289
+ )
1290
+ : new Map<string, string>(),
1291
+ ]);
1292
+ const pageItems = includeExcerpts
1293
+ ? serializedItems.map((item) => ({
1294
+ ...item,
1295
+ excerpt: item.document.canView
1296
+ ? buildListRowExcerpt({
1297
+ content: bodyPrefixByDocumentId.get(item.document.id),
1298
+ title: item.document.title,
1299
+ })
1300
+ : "",
1301
+ }))
1302
+ : serializedItems;
1245
1303
  const organizationVisibleDocumentIds =
1246
1304
  organizationFilesItemFilter && sourceSnapshots.length > 0
1247
1305
  ? new Set(
@@ -1282,7 +1340,7 @@ export async function getContentDatabasePageResponse(
1282
1340
  const pagedPrimary = pagedSources[0] ?? null;
1283
1341
 
1284
1342
  const federatedItems = federateSources({
1285
- items: serializedItems,
1343
+ items: pageItems,
1286
1344
  sources: pagedSources,
1287
1345
  });
1288
1346
  const itemsWithOverlay = applyFederatedOverlayValues(
@@ -1639,15 +1697,40 @@ export async function deleteDatabaseDataForDocument(
1639
1697
  .select({ id: schema.contentDatabaseSources.id })
1640
1698
  .from(schema.contentDatabaseSources)
1641
1699
  .where(eq(schema.contentDatabaseSources.databaseId, database.id));
1700
+ const deletedBlobReferences = new Set<string>();
1642
1701
  for (const source of sources) {
1643
1702
  await db
1644
1703
  .delete(schema.contentDatabaseBodyHydrationQueue)
1645
1704
  .where(
1646
1705
  eq(schema.contentDatabaseBodyHydrationQueue.sourceId, source.id),
1647
1706
  );
1648
- await db
1649
- .delete(schema.contentDatabaseSourceExecutions)
1650
- .where(eq(schema.contentDatabaseSourceExecutions.sourceId, source.id));
1707
+ await db.transaction(async (tx) => {
1708
+ const deletedExecutions = await tx
1709
+ .delete(schema.contentDatabaseSourceExecutions)
1710
+ .where(eq(schema.contentDatabaseSourceExecutions.sourceId, source.id))
1711
+ .returning({
1712
+ ownerEmail: schema.contentDatabaseSourceExecutions.ownerEmail,
1713
+ payloadJson: schema.contentDatabaseSourceExecutions.payloadJson,
1714
+ sourceId: schema.contentDatabaseSourceExecutions.sourceId,
1715
+ });
1716
+ for (const row of deletedExecutions) {
1717
+ const reference = builderExecutionPayloadReference(row.payloadJson);
1718
+ if (
1719
+ reference &&
1720
+ isBuilderPrivatePayloadBoundToSource(
1721
+ reference,
1722
+ row.ownerEmail,
1723
+ row.sourceId,
1724
+ )
1725
+ ) {
1726
+ deletedBlobReferences.add(reference);
1727
+ await enqueueBuilderPrivatePayloadCleanup(
1728
+ tx as unknown as ReturnType<typeof getDb>,
1729
+ reference,
1730
+ );
1731
+ }
1732
+ }
1733
+ });
1651
1734
  await db
1652
1735
  .delete(schema.contentDatabaseSourceChangeReviews)
1653
1736
  .where(
@@ -1663,6 +1746,9 @@ export async function deleteDatabaseDataForDocument(
1663
1746
  .delete(schema.contentDatabaseSourceFields)
1664
1747
  .where(eq(schema.contentDatabaseSourceFields.sourceId, source.id));
1665
1748
  }
1749
+ for (const reference of deletedBlobReferences) {
1750
+ await cleanupBuilderPrivatePayload(reference, "deleted database source");
1751
+ }
1666
1752
  await db
1667
1753
  .delete(schema.contentDatabaseSources)
1668
1754
  .where(eq(schema.contentDatabaseSources.databaseId, database.id));