@agent-native/core 0.101.6 → 0.101.10

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 (237) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +28 -0
  3. package/corpus/core/docs/content/deployment.mdx +9 -7
  4. package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +9 -7
  5. package/corpus/core/docs/content/locales/de-DE/deployment.mdx +9 -7
  6. package/corpus/core/docs/content/locales/es-ES/deployment.mdx +9 -7
  7. package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +9 -7
  8. package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +9 -7
  9. package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +9 -7
  10. package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +9 -7
  11. package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +9 -7
  12. package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +9 -7
  13. package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +9 -7
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/a2a/artifact-response.ts +255 -12
  16. package/corpus/core/src/agent/thread-data-builder.ts +84 -11
  17. package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
  18. package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
  19. package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
  20. package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
  21. package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
  22. package/corpus/core/src/integrations/a2a-continuation-processor.ts +99 -7
  23. package/corpus/core/src/integrations/adapters/discord.ts +3 -1
  24. package/corpus/core/src/integrations/adapters/email.ts +4 -1
  25. package/corpus/core/src/integrations/adapters/google-docs.ts +4 -1
  26. package/corpus/core/src/integrations/adapters/microsoft-teams.ts +3 -1
  27. package/corpus/core/src/integrations/adapters/slack.ts +38 -9
  28. package/corpus/core/src/integrations/adapters/telegram.ts +32 -10
  29. package/corpus/core/src/integrations/adapters/whatsapp.ts +5 -1
  30. package/corpus/core/src/integrations/index.ts +1 -0
  31. package/corpus/core/src/integrations/types.ts +9 -2
  32. package/corpus/core/src/integrations/webhook-handler.ts +69 -45
  33. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +1 -0
  34. package/corpus/core/src/server/auth.ts +25 -1
  35. package/corpus/core/src/server/builder-browser.ts +353 -1
  36. package/corpus/core/src/server/core-routes-plugin.ts +601 -223
  37. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
  38. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
  39. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
  40. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
  41. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
  42. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
  43. package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
  44. package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
  45. package/corpus/templates/content/.agents/skills/content/SKILL.md +31 -0
  46. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +35 -21
  47. package/corpus/templates/content/AGENTS.md +32 -9
  48. package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
  49. package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
  50. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
  51. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
  52. package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
  53. package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
  54. package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
  55. package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
  56. package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
  57. package/corpus/templates/content/actions/_database-source-utils.ts +1071 -170
  58. package/corpus/templates/content/actions/_database-utils.ts +104 -13
  59. package/corpus/templates/content/actions/_local-file-documents.ts +53 -1
  60. package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
  61. package/corpus/templates/content/actions/_property-utils.ts +7 -1
  62. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
  63. package/corpus/templates/content/actions/configure-document-property.ts +11 -0
  64. package/corpus/templates/content/actions/create-content-database.ts +11 -0
  65. package/corpus/templates/content/actions/create-document.ts +9 -0
  66. package/corpus/templates/content/actions/create-inline-content-database.ts +3 -0
  67. package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
  68. package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
  69. package/corpus/templates/content/actions/export-content-source.ts +1 -0
  70. package/corpus/templates/content/actions/get-content-database.ts +13 -1
  71. package/corpus/templates/content/actions/get-document.ts +12 -2
  72. package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
  73. package/corpus/templates/content/actions/import-content-source.ts +6 -0
  74. package/corpus/templates/content/actions/list-documents.ts +3 -0
  75. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
  76. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
  77. package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
  78. package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
  79. package/corpus/templates/content/actions/pull-document.ts +2 -0
  80. package/corpus/templates/content/actions/search-documents.ts +5 -1
  81. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
  82. package/corpus/templates/content/actions/update-document.ts +16 -1
  83. package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
  84. package/corpus/templates/content/actions/view-screen.ts +15 -2
  85. package/corpus/templates/content/app/components/editor/DescriptionField.tsx +121 -0
  86. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +14 -0
  87. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +250 -49
  88. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
  89. package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
  90. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2414 -589
  91. package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
  92. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
  93. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
  94. package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
  95. package/corpus/templates/content/app/global.css +18 -1
  96. package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
  97. package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
  98. package/corpus/templates/content/app/i18n/zh-TW.ts +30 -0
  99. package/corpus/templates/content/app/i18n-data.ts +339 -0
  100. package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
  101. package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
  102. package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
  103. package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
  104. package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
  105. package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
  106. package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
  107. package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
  108. package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
  109. package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
  110. package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
  111. package/corpus/templates/content/changelog/2026-07-14-pages-and-databases-can-describe-themselves-with-guidance-fo.md +6 -0
  112. package/corpus/templates/content/changelog/2026-07-14-slack-follow-ups-now-retain-created-content-identity-and-per.md +6 -0
  113. package/corpus/templates/content/parity/matrix.md +27 -25
  114. package/corpus/templates/content/parity/matrix.ts +50 -2
  115. package/corpus/templates/content/server/db/schema.ts +51 -0
  116. package/corpus/templates/content/server/lib/document-context.ts +76 -0
  117. package/corpus/templates/content/server/lib/public-documents.ts +3 -0
  118. package/corpus/templates/content/server/plugins/auth.ts +1 -0
  119. package/corpus/templates/content/server/plugins/db.ts +59 -0
  120. package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +5 -0
  121. package/corpus/templates/content/shared/api.ts +87 -1
  122. package/corpus/templates/content/shared/builder-mdx.ts +705 -15
  123. package/corpus/templates/content/shared/content-source.ts +7 -0
  124. package/corpus/templates/content/shared/nfm.ts +3 -3
  125. package/corpus/templates/content/shared/properties.ts +2 -0
  126. package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
  127. package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
  128. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
  129. package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
  130. package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
  131. package/corpus/templates/design/app/i18n/en-US.ts +1 -0
  132. package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
  133. package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
  134. package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
  135. package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
  136. package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
  137. package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
  138. package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
  139. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  140. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
  141. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
  142. package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
  143. package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
  144. package/corpus/templates/design/shared/review-anchor.ts +48 -0
  145. package/corpus/templates/plan/server/plan-mdx.ts +53 -7
  146. package/dist/a2a/artifact-response.d.ts +15 -0
  147. package/dist/a2a/artifact-response.d.ts.map +1 -1
  148. package/dist/a2a/artifact-response.js +204 -5
  149. package/dist/a2a/artifact-response.js.map +1 -1
  150. package/dist/agent/thread-data-builder.d.ts +6 -0
  151. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  152. package/dist/agent/thread-data-builder.js +71 -8
  153. package/dist/agent/thread-data-builder.js.map +1 -1
  154. package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
  155. package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
  156. package/dist/client/review/ReviewCommentComposer.js +3 -3
  157. package/dist/client/review/ReviewCommentComposer.js.map +1 -1
  158. package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
  159. package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
  160. package/dist/client/review/ReviewThreadPanel.js +6 -4
  161. package/dist/client/review/ReviewThreadPanel.js.map +1 -1
  162. package/dist/client/settings/SettingsPanel.d.ts +3 -0
  163. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  164. package/dist/client/settings/SettingsPanel.js +11 -4
  165. package/dist/client/settings/SettingsPanel.js.map +1 -1
  166. package/dist/client/settings/useBuilderStatus.d.ts +8 -4
  167. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  168. package/dist/client/settings/useBuilderStatus.js +19 -6
  169. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  170. package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
  171. package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
  172. package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
  173. package/dist/collab/awareness.d.ts +2 -2
  174. package/dist/collab/awareness.d.ts.map +1 -1
  175. package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
  176. package/dist/integrations/a2a-continuation-processor.js +72 -6
  177. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  178. package/dist/integrations/adapters/discord.d.ts.map +1 -1
  179. package/dist/integrations/adapters/discord.js +1 -0
  180. package/dist/integrations/adapters/discord.js.map +1 -1
  181. package/dist/integrations/adapters/email.d.ts.map +1 -1
  182. package/dist/integrations/adapters/email.js +2 -0
  183. package/dist/integrations/adapters/email.js.map +1 -1
  184. package/dist/integrations/adapters/google-docs.d.ts.map +1 -1
  185. package/dist/integrations/adapters/google-docs.js +2 -0
  186. package/dist/integrations/adapters/google-docs.js.map +1 -1
  187. package/dist/integrations/adapters/microsoft-teams.d.ts.map +1 -1
  188. package/dist/integrations/adapters/microsoft-teams.js +1 -0
  189. package/dist/integrations/adapters/microsoft-teams.js.map +1 -1
  190. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  191. package/dist/integrations/adapters/slack.js +21 -6
  192. package/dist/integrations/adapters/slack.js.map +1 -1
  193. package/dist/integrations/adapters/telegram.d.ts.map +1 -1
  194. package/dist/integrations/adapters/telegram.js +11 -2
  195. package/dist/integrations/adapters/telegram.js.map +1 -1
  196. package/dist/integrations/adapters/whatsapp.d.ts.map +1 -1
  197. package/dist/integrations/adapters/whatsapp.js +3 -0
  198. package/dist/integrations/adapters/whatsapp.js.map +1 -1
  199. package/dist/integrations/index.d.ts +1 -1
  200. package/dist/integrations/index.d.ts.map +1 -1
  201. package/dist/integrations/index.js.map +1 -1
  202. package/dist/integrations/types.d.ts +8 -2
  203. package/dist/integrations/types.d.ts.map +1 -1
  204. package/dist/integrations/types.js.map +1 -1
  205. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  206. package/dist/integrations/webhook-handler.js +40 -43
  207. package/dist/integrations/webhook-handler.js.map +1 -1
  208. package/dist/observability/routes.d.ts +5 -5
  209. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  210. package/dist/secrets/routes.d.ts +9 -9
  211. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  212. package/dist/server/agent-chat/action-filters-a2a.js +1 -0
  213. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  214. package/dist/server/auth.d.ts +1 -0
  215. package/dist/server/auth.d.ts.map +1 -1
  216. package/dist/server/auth.js +19 -2
  217. package/dist/server/auth.js.map +1 -1
  218. package/dist/server/builder-browser.d.ts +78 -0
  219. package/dist/server/builder-browser.d.ts.map +1 -1
  220. package/dist/server/builder-browser.js +246 -1
  221. package/dist/server/builder-browser.js.map +1 -1
  222. package/dist/server/core-routes-plugin.d.ts +41 -0
  223. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  224. package/dist/server/core-routes-plugin.js +260 -14
  225. package/dist/server/core-routes-plugin.js.map +1 -1
  226. package/docs/content/deployment.mdx +9 -7
  227. package/docs/content/locales/ar-SA/deployment.mdx +9 -7
  228. package/docs/content/locales/de-DE/deployment.mdx +9 -7
  229. package/docs/content/locales/es-ES/deployment.mdx +9 -7
  230. package/docs/content/locales/fr-FR/deployment.mdx +9 -7
  231. package/docs/content/locales/hi-IN/deployment.mdx +9 -7
  232. package/docs/content/locales/ja-JP/deployment.mdx +9 -7
  233. package/docs/content/locales/ko-KR/deployment.mdx +9 -7
  234. package/docs/content/locales/pt-BR/deployment.mdx +9 -7
  235. package/docs/content/locales/zh-CN/deployment.mdx +9 -7
  236. package/docs/content/locales/zh-TW/deployment.mdx +9 -7
  237. package/package.json +2 -2
@@ -14,6 +14,10 @@ import {
14
14
  type DocumentPropertyValue,
15
15
  type ExecuteBuilderSourceExecutionRequest,
16
16
  } from "../shared/api.js";
17
+ import {
18
+ lookupBuilderCmsSafeModelIntent,
19
+ type BuilderCmsIntentMatch,
20
+ } from "./_builder-cms-intent-lookup.js";
17
21
  import {
18
22
  type BuilderCmsEntryLiveState,
19
23
  readBuilderCmsEntryLiveState,
@@ -33,11 +37,13 @@ import {
33
37
  builderCmsExecutionIdempotencyKey,
34
38
  resolveBuilderCmsExecutionPushMode,
35
39
  validateBuilderCmsExecutionDryRun,
40
+ BUILDER_CMS_EXECUTION_MARKER_FIELD,
36
41
  } from "./_builder-cms-write-adapter.js";
37
42
  import {
38
43
  type BuilderCmsWriteResult,
39
44
  executeBuilderCmsWrite,
40
45
  } from "./_builder-cms-write-client.js";
46
+ import { createBuilderSourceTiming } from "./_builder-source-timings.js";
41
47
  import {
42
48
  getContentDatabaseSourceSnapshotForWrite,
43
49
  resolveDatabaseForSourceMutation,
@@ -53,6 +59,7 @@ export interface BuilderSourceExecutionRecord {
53
59
  state: ContentDatabaseSourceExecutionState | string;
54
60
  idempotencyKey: string;
55
61
  payloadJson: string;
62
+ attemptToken?: string | null;
56
63
  updatedAt: string;
57
64
  }
58
65
 
@@ -69,6 +76,7 @@ export interface ExecuteBuilderSourceExecutionDeps {
69
76
  database: DatabaseRecord,
70
77
  ) => Promise<ContentDatabaseSource | null>;
71
78
  getExecution: (args: {
79
+ ownerEmail: string;
72
80
  sourceId: string;
73
81
  changeSetId: string;
74
82
  idempotencyKey: string;
@@ -87,6 +95,13 @@ export interface ExecuteBuilderSourceExecutionDeps {
87
95
  payload: unknown;
88
96
  now: string;
89
97
  staleBefore: string;
98
+ attemptToken: string;
99
+ }) => Promise<boolean>;
100
+ checkpointResponse?: (args: {
101
+ executionId: string;
102
+ attemptToken: string;
103
+ payload: unknown;
104
+ now: string;
90
105
  }) => Promise<boolean>;
91
106
  markExecutionSucceeded: (args: {
92
107
  executionId: string;
@@ -94,6 +109,7 @@ export interface ExecuteBuilderSourceExecutionDeps {
94
109
  summary: string;
95
110
  payload: unknown;
96
111
  now: string;
112
+ attemptToken?: string;
97
113
  }) => Promise<void>;
98
114
  markExecutionFailed: (args: {
99
115
  executionId: string;
@@ -101,6 +117,8 @@ export interface ExecuteBuilderSourceExecutionDeps {
101
117
  payload: unknown;
102
118
  lastError: string;
103
119
  now: string;
120
+ attemptToken?: string;
121
+ state?: "failed" | "reconciliation_required";
104
122
  }) => Promise<void>;
105
123
  executeWrite: (args: {
106
124
  request: BuilderCmsExecutionPayload["request"];
@@ -118,6 +136,15 @@ export interface ExecuteBuilderSourceExecutionDeps {
118
136
  now: string;
119
137
  }) => Promise<void>;
120
138
  getResponse: (databaseId: string) => Promise<ContentDatabaseResponse>;
139
+ lookupSafeModelIntent?: (args: {
140
+ marker?: string;
141
+ exactTitle?: string;
142
+ intendedFields?: Record<string, unknown>;
143
+ }) => Promise<{
144
+ count: number;
145
+ matchingIntentCount?: number;
146
+ matches: BuilderCmsIntentMatch[];
147
+ }>;
121
148
  }
122
149
 
123
150
  const RUNNING_EXECUTION_TIMEOUT_MS = 10 * 60 * 1000;
@@ -182,11 +209,34 @@ function staleRunningCutoff(now: string) {
182
209
  ).toISOString();
183
210
  }
184
211
 
212
+ export function builderExecutionAffectedRows(result: unknown) {
213
+ return (
214
+ (
215
+ result as {
216
+ rowsAffected?: number;
217
+ changes?: number;
218
+ rowCount?: number | null;
219
+ }
220
+ ).rowsAffected ??
221
+ (result as { changes?: number }).changes ??
222
+ (result as { rowCount?: number | null }).rowCount ??
223
+ 0
224
+ );
225
+ }
226
+
227
+ export function builderExecutionConflict(message: string) {
228
+ return Object.assign(new Error(message), { statusCode: 409 });
229
+ }
230
+
185
231
  function isReclaimableRunningExecution(
186
232
  execution: BuilderSourceExecutionRecord,
187
233
  now: string,
188
234
  ) {
189
- if (execution.state !== "running") return true;
235
+ if (
236
+ execution.state !== "running" &&
237
+ execution.state !== "reconciliation_required"
238
+ )
239
+ return true;
190
240
  const updatedAt = Date.parse(execution.updatedAt);
191
241
  if (!Number.isFinite(updatedAt)) return false;
192
242
  return updatedAt < Date.parse(staleRunningCutoff(now));
@@ -208,6 +258,40 @@ function executionResponsePayload(args: {
208
258
  };
209
259
  }
210
260
 
261
+ function createDraftIntent(request: BuilderCmsExecutionPayload["request"]) {
262
+ if (request.method !== "POST") return null;
263
+ const body = recordValue(request.body);
264
+ const data = recordValue(body?.data);
265
+ if (!data) return null;
266
+ const marker =
267
+ typeof data[BUILDER_CMS_EXECUTION_MARKER_FIELD] === "string"
268
+ ? data[BUILDER_CMS_EXECUTION_MARKER_FIELD]
269
+ : undefined;
270
+ const exactTitle = typeof data.title === "string" ? data.title : undefined;
271
+ const intendedFields = Object.fromEntries(
272
+ Object.entries(data).filter(
273
+ ([key]) => key !== BUILDER_CMS_EXECUTION_MARKER_FIELD,
274
+ ),
275
+ );
276
+ return { marker, exactTitle, intendedFields };
277
+ }
278
+
279
+ function recoveredWriteResult(
280
+ match: BuilderCmsIntentMatch,
281
+ ): BuilderCmsWriteResult {
282
+ return {
283
+ ok: true,
284
+ status: 200,
285
+ entryId: match.id,
286
+ responseBody: {
287
+ id: match.id,
288
+ lastUpdated: match.lastUpdated,
289
+ published: match.published,
290
+ recoveredByReadOnlyLookup: true,
291
+ },
292
+ };
293
+ }
294
+
211
295
  function proposedSourceDisplayKey(
212
296
  changeSet: ContentDatabaseSourceChangeSet,
213
297
  fallback: string,
@@ -275,10 +359,11 @@ function parseSourceValues(
275
359
  }
276
360
  }
277
361
 
278
- function builderCmsReconciledSourceValuesJson(args: {
362
+ export function builderCmsReconciledSourceValuesJson(args: {
279
363
  existingSourceValuesJson: string | null | undefined;
280
364
  snapshotSourceValues: Record<string, DocumentPropertyValue> | undefined;
281
365
  changeSet: ContentDatabaseSourceChangeSet;
366
+ plan: BuilderCmsExecutionPlan;
282
367
  }) {
283
368
  const next = {
284
369
  ...(args.snapshotSourceValues ?? {}),
@@ -299,6 +384,32 @@ function builderCmsReconciledSourceValuesJson(args: {
299
384
  next[BUILDER_CMS_BODY_SIDECARS_KEY] = bodyChange.sidecarsJson;
300
385
  }
301
386
  }
387
+ const requestData = args.plan.payload.request.body.data;
388
+ if (
389
+ requestData &&
390
+ typeof requestData === "object" &&
391
+ !Array.isArray(requestData)
392
+ ) {
393
+ for (const [fieldName, value] of Object.entries(requestData)) {
394
+ // Builder blocks remain in body/blob storage. The compact body hash above
395
+ // is the durable proof used by later required-field validation.
396
+ if (fieldName === "blocks") continue;
397
+ const sourceFieldKey = `data.${fieldName}`;
398
+ next[sourceFieldKey] = value as DocumentPropertyValue;
399
+ if (
400
+ value &&
401
+ typeof value === "object" &&
402
+ !Array.isArray(value) &&
403
+ (value as Record<string, unknown>)["@type"] ===
404
+ "@builder.io/core:Reference" &&
405
+ typeof (value as Record<string, unknown>).id === "string"
406
+ ) {
407
+ next[`__agent_native_builder_reference_id:${sourceFieldKey}`] = (
408
+ value as Record<string, unknown>
409
+ ).id as string;
410
+ }
411
+ }
412
+ }
302
413
  return JSON.stringify(next);
303
414
  }
304
415
 
@@ -476,6 +587,7 @@ async function reconcileBuilderCmsWrite(args: {
476
587
  existingSourceValuesJson: row?.sourceValuesJson,
477
588
  snapshotSourceValues: snapshotRow?.sourceValues,
478
589
  changeSet: args.changeSet,
590
+ plan: args.plan,
479
591
  });
480
592
  const patchWithValues = {
481
593
  ...patch,
@@ -535,22 +647,49 @@ export function realExecutionDeps(
535
647
  getSourceSnapshot: (database) =>
536
648
  getContentDatabaseSourceSnapshotForWrite(database, sourceId),
537
649
  getExecution: async (args) => {
538
- const [execution] = await getDb()
539
- .select()
540
- .from(schema.contentDatabaseSourceExecutions)
650
+ const [claim] = await getDb()
651
+ .select({
652
+ executionId: schema.contentDatabaseSourceExecutionClaims.executionId,
653
+ })
654
+ .from(schema.contentDatabaseSourceExecutionClaims)
541
655
  .where(
542
656
  and(
543
- eq(schema.contentDatabaseSourceExecutions.sourceId, args.sourceId),
544
657
  eq(
545
- schema.contentDatabaseSourceExecutions.changeSetId,
546
- args.changeSetId,
658
+ schema.contentDatabaseSourceExecutionClaims.ownerEmail,
659
+ args.ownerEmail,
547
660
  ),
548
661
  eq(
549
- schema.contentDatabaseSourceExecutions.idempotencyKey,
662
+ schema.contentDatabaseSourceExecutionClaims.sourceId,
663
+ args.sourceId,
664
+ ),
665
+ eq(
666
+ schema.contentDatabaseSourceExecutionClaims.idempotencyKey,
550
667
  args.idempotencyKey,
551
668
  ),
552
669
  ),
553
- );
670
+ )
671
+ .limit(1);
672
+ if (!claim) return null;
673
+ const [execution] = await getDb()
674
+ .select()
675
+ .from(schema.contentDatabaseSourceExecutions)
676
+ .where(
677
+ and(
678
+ eq(schema.contentDatabaseSourceExecutions.id, claim.executionId),
679
+ eq(
680
+ schema.contentDatabaseSourceExecutions.ownerEmail,
681
+ args.ownerEmail,
682
+ ),
683
+ ),
684
+ )
685
+ .limit(1);
686
+ if (
687
+ execution?.sourceId !== args.sourceId ||
688
+ execution?.changeSetId !== args.changeSetId ||
689
+ execution?.idempotencyKey !== args.idempotencyKey
690
+ ) {
691
+ return null;
692
+ }
554
693
  return execution ?? null;
555
694
  },
556
695
  updateExecutionState: async (args) => {
@@ -573,6 +712,7 @@ export function realExecutionDeps(
573
712
  summary: args.summary,
574
713
  payloadJson: JSON.stringify(args.payload),
575
714
  lastError: null,
715
+ attemptToken: args.attemptToken,
576
716
  updatedAt: args.now,
577
717
  })
578
718
  .where(
@@ -593,45 +733,93 @@ export function realExecutionDeps(
593
733
  ),
594
734
  ),
595
735
  );
596
- const changes =
597
- (result as { rowsAffected?: number; changes?: number }).rowsAffected ??
598
- (result as { rowsAffected?: number; changes?: number }).changes ??
599
- 0;
600
- return changes > 0;
736
+ return builderExecutionAffectedRows(result) > 0;
601
737
  },
602
- markExecutionSucceeded: async (args) => {
603
- const db = getDb();
604
- await db
738
+ checkpointResponse: async (args) => {
739
+ const result = await getDb()
605
740
  .update(schema.contentDatabaseSourceExecutions)
606
741
  .set({
607
- state: "succeeded",
608
- summary: args.summary,
742
+ state: "response_received",
743
+ summary: "Builder response received; reconciling locally.",
609
744
  payloadJson: JSON.stringify(args.payload),
610
745
  lastError: null,
611
746
  updatedAt: args.now,
612
747
  })
613
- .where(eq(schema.contentDatabaseSourceExecutions.id, args.executionId));
614
- await db
615
- .update(schema.contentDatabaseSourceChangeSets)
616
- .set({ state: "applied", updatedAt: args.now })
617
- .where(eq(schema.contentDatabaseSourceChangeSets.id, args.changeSetId));
748
+ .where(
749
+ and(
750
+ eq(schema.contentDatabaseSourceExecutions.id, args.executionId),
751
+ eq(
752
+ schema.contentDatabaseSourceExecutions.attemptToken,
753
+ args.attemptToken,
754
+ ),
755
+ ),
756
+ );
757
+ return builderExecutionAffectedRows(result) > 0;
758
+ },
759
+ markExecutionSucceeded: async (args) => {
760
+ const db = getDb();
761
+ await db.transaction(async (tx) => {
762
+ const result = await tx
763
+ .update(schema.contentDatabaseSourceExecutions)
764
+ .set({
765
+ state: "succeeded",
766
+ summary: args.summary,
767
+ payloadJson: JSON.stringify(args.payload),
768
+ lastError: null,
769
+ updatedAt: args.now,
770
+ })
771
+ .where(
772
+ args.attemptToken
773
+ ? and(
774
+ eq(
775
+ schema.contentDatabaseSourceExecutions.id,
776
+ args.executionId,
777
+ ),
778
+ eq(
779
+ schema.contentDatabaseSourceExecutions.attemptToken,
780
+ args.attemptToken,
781
+ ),
782
+ )
783
+ : eq(schema.contentDatabaseSourceExecutions.id, args.executionId),
784
+ );
785
+ if (args.attemptToken && builderExecutionAffectedRows(result) === 0) {
786
+ throw new Error("Execution lease was reclaimed before completion.");
787
+ }
788
+ await tx
789
+ .update(schema.contentDatabaseSourceChangeSets)
790
+ .set({ state: "applied", updatedAt: args.now })
791
+ .where(
792
+ eq(schema.contentDatabaseSourceChangeSets.id, args.changeSetId),
793
+ );
794
+ });
618
795
  },
619
796
  markExecutionFailed: async (args) => {
620
797
  await getDb()
621
798
  .update(schema.contentDatabaseSourceExecutions)
622
799
  .set({
623
- state: "failed",
800
+ state: args.state ?? "failed",
624
801
  summary: args.summary,
625
802
  payloadJson: JSON.stringify(args.payload),
626
803
  lastError: args.lastError,
627
804
  updatedAt: args.now,
628
805
  })
629
- .where(eq(schema.contentDatabaseSourceExecutions.id, args.executionId));
806
+ .where(
807
+ args.attemptToken
808
+ ? and(
809
+ eq(schema.contentDatabaseSourceExecutions.id, args.executionId),
810
+ eq(
811
+ schema.contentDatabaseSourceExecutions.attemptToken,
812
+ args.attemptToken,
813
+ ),
814
+ )
815
+ : eq(schema.contentDatabaseSourceExecutions.id, args.executionId),
816
+ );
630
817
  },
631
818
  executeWrite: (args) => executeBuilderCmsWrite(args),
632
819
  readLiveEntry: (args) => readBuilderCmsEntryLiveState(args),
633
820
  reconcileWrite: reconcileBuilderCmsWrite,
634
821
  getResponse: (databaseId) => getContentDatabaseResponse(databaseId),
822
+ lookupSafeModelIntent: lookupBuilderCmsSafeModelIntent,
635
823
  };
636
824
  }
637
825
 
@@ -639,290 +827,446 @@ export async function executeBuilderSourceExecutionWithDeps(
639
827
  args: ExecuteBuilderSourceExecutionRequest,
640
828
  deps: ExecuteBuilderSourceExecutionDeps,
641
829
  ) {
642
- const database = await deps.resolveDatabase(args);
643
- if (!database) throw new Error("Database not found.");
644
- await deps.assertEditor(database);
645
-
646
- const source = await deps.getSourceSnapshot(database);
647
- if (!source || source.sourceType !== "builder-cms") {
648
- throw new Error("Attach a Builder CMS source before executing a write.");
649
- }
650
-
651
- const changeSet = source.changeSets.find(
652
- (candidate) => candidate.id === args.changeSetId,
653
- );
654
- if (!changeSet) throw new Error("Source change-set not found.");
655
- if (changeSet.direction !== "outbound") {
656
- throw new Error("Only outbound Builder change sets can be executed.");
657
- }
830
+ const timing = createBuilderSourceTiming("execute_builder_source_execution");
831
+ try {
832
+ const { database, source } = await timing.measure(
833
+ "snapshot_read_and_diff_load",
834
+ async () => {
835
+ const database = await deps.resolveDatabase(args);
836
+ if (!database) throw new Error("Database not found.");
837
+ await deps.assertEditor(database);
838
+ const source = await deps.getSourceSnapshot(database);
839
+ return { database, source };
840
+ },
841
+ );
842
+ if (!source || source.sourceType !== "builder-cms") {
843
+ throw new Error("Attach a Builder CMS source before executing a write.");
844
+ }
845
+ if (source.sourceTable !== BUILDER_CMS_SAFE_WRITE_MODEL) {
846
+ throw new Error(
847
+ `Live Builder writes are only allowed for ${BUILDER_CMS_SAFE_WRITE_MODEL}.`,
848
+ );
849
+ }
658
850
 
659
- const resolvedPushMode = resolveBuilderCmsExecutionPushMode({
660
- source,
661
- changeSet,
662
- });
663
- const effectivePushMode =
664
- resolvedPushMode === "none" ? "autosave" : resolvedPushMode;
665
- const pushMode = executablePushMode(
666
- args.pushModeConfirmation ?? effectivePushMode,
667
- );
668
- if (!pushMode) {
669
- throw new Error(
670
- "Builder execution requires Autosave, Draft, or Publish push mode.",
851
+ const gateStartedAt = timing.start();
852
+ const changeSet = source.changeSets.find(
853
+ (candidate) => candidate.id === args.changeSetId,
671
854
  );
672
- }
673
- // The gate key is keyed on the RAW resolved push mode (matching the plan in
674
- // buildBuilderCmsExecutionPlan) NOT on pushModeConfirmation. Keying on the
675
- // confirmation would let a caller's confirmation diverge the key from the
676
- // prepared gate; the confirmation is still validated inside the plan below.
677
- const expectedKey = builderCmsExecutionIdempotencyKey({
678
- sourceId: source.id,
679
- changeSetId: changeSet.id,
680
- pushMode: resolvedPushMode,
681
- });
682
- if (args.idempotencyKey && args.idempotencyKey !== expectedKey) {
683
- throw new Error(
684
- "Execution idempotency key does not match this write plan.",
855
+ if (!changeSet) throw new Error("Source change-set not found.");
856
+ if (changeSet.direction !== "outbound") {
857
+ throw new Error("Only outbound Builder change sets can be executed.");
858
+ }
859
+
860
+ const resolvedPushMode = resolveBuilderCmsExecutionPushMode({
861
+ source,
862
+ changeSet,
863
+ });
864
+ const effectivePushMode =
865
+ resolvedPushMode === "none" ? "autosave" : resolvedPushMode;
866
+ const pushMode = executablePushMode(
867
+ args.pushModeConfirmation ?? effectivePushMode,
685
868
  );
686
- }
869
+ if (!pushMode) {
870
+ throw new Error(
871
+ "Builder execution requires Autosave, Draft, or Publish push mode.",
872
+ );
873
+ }
874
+ // The gate key is keyed on the RAW resolved push mode (matching the plan in
875
+ // buildBuilderCmsExecutionPlan) — NOT on pushModeConfirmation. Keying on the
876
+ // confirmation would let a caller's confirmation diverge the key from the
877
+ // prepared gate; the confirmation is still validated inside the plan below.
878
+ const expectedKey = builderCmsExecutionIdempotencyKey({
879
+ sourceId: source.id,
880
+ changeSetId: changeSet.id,
881
+ pushMode: resolvedPushMode,
882
+ });
883
+ if (args.idempotencyKey && args.idempotencyKey !== expectedKey) {
884
+ throw new Error(
885
+ "Execution idempotency key does not match this write plan.",
886
+ );
887
+ }
687
888
 
688
- const execution = await deps.getExecution({
689
- sourceId: source.id,
690
- changeSetId: changeSet.id,
691
- idempotencyKey: expectedKey,
692
- });
693
- if (!execution) {
694
- throw new Error("Prepare the Builder execution gate before executing it.");
695
- }
696
- const now = deps.now();
697
- if (execution.state === "succeeded") {
698
- return deps.getResponse(database.id);
699
- }
700
- if (!isReclaimableRunningExecution(execution, now)) {
701
- throw new Error("Builder execution is already running.");
702
- }
889
+ const execution = await deps.getExecution({
890
+ ownerEmail: database.ownerEmail,
891
+ sourceId: source.id,
892
+ changeSetId: changeSet.id,
893
+ idempotencyKey: expectedKey,
894
+ });
895
+ if (!execution) {
896
+ throw new Error(
897
+ "Prepare the Builder execution gate before executing it.",
898
+ );
899
+ }
900
+ const now = deps.now();
901
+ if (execution.state === "succeeded") {
902
+ timing.record("approval_gate_and_dry_run_validation", gateStartedAt);
903
+ timing.ensure("write_dispatch");
904
+ const response = await timing.measure(
905
+ "reconciliation_and_persistence",
906
+ () => deps.getResponse(database.id),
907
+ );
908
+ const result = { ...response, timings: timing.finish() };
909
+ timing.log("succeeded");
910
+ return result;
911
+ }
912
+ const initialStoredPayload = parsePayload(execution.payloadJson);
913
+ const hasSuccessfulResponse = Boolean(
914
+ successfulStoredWriteResult(initialStoredPayload),
915
+ );
916
+ if (
917
+ !hasSuccessfulResponse &&
918
+ !isReclaimableRunningExecution(execution, now)
919
+ ) {
920
+ throw builderExecutionConflict(
921
+ execution.state === "reconciliation_required"
922
+ ? "Builder reconciliation required; wait for the recovery window before a read-only lookup. Do not retry."
923
+ : "Builder execution is already running.",
924
+ );
925
+ }
703
926
 
704
- if (changeSet.state !== "approved") {
705
- throw new Error("Approve the Builder change set before executing it.");
706
- }
927
+ if (changeSet.state !== "approved") {
928
+ throw new Error("Approve the Builder change set before executing it.");
929
+ }
707
930
 
708
- const plan = buildBuilderCmsExecutionPlan({
709
- source,
710
- changeSet,
711
- pushModeConfirmation: pushMode,
712
- publicationTransition: args.publicationTransition,
713
- confirmUnpublish: args.confirmUnpublish,
714
- });
715
- const storedPayload = parsePayload(execution.payloadJson);
716
- const validatedPayload = validateBuilderCmsExecutionDryRun({
717
- storedPayload,
718
- plan,
719
- now,
720
- });
721
- const dryRun = validatedPayload.dryRun;
722
- if (dryRun?.status !== "validated") {
723
- const message =
724
- dryRun?.status === "stale" && dryRun.mismatches.length > 0
725
- ? dryRun.mismatches.join(" ")
726
- : (plan.lastError ?? "Builder execution is blocked.");
727
- await deps.updateExecutionState({
728
- executionId: execution.id,
729
- state: "blocked",
730
- summary: `${plan.summary} Execution blocked before write.`,
731
- payload: validatedPayload,
732
- lastError: message,
733
- now,
931
+ const plan = buildBuilderCmsExecutionPlan({
932
+ source,
933
+ changeSet,
934
+ pushModeConfirmation: pushMode,
935
+ publicationTransition: args.publicationTransition,
936
+ confirmUnpublish: args.confirmUnpublish,
734
937
  });
735
- throw new Error(message);
736
- }
737
- if (plan.state !== "ready") {
738
- const message = plan.lastError ?? "Builder execution is not ready.";
739
- await deps.updateExecutionState({
740
- executionId: execution.id,
741
- state: plan.state,
742
- summary: plan.summary,
743
- payload: validatedPayload,
744
- lastError: message,
938
+ const storedPayload = initialStoredPayload;
939
+ const storedRequest = recordValue(storedPayload.request) as
940
+ | BuilderCmsExecutionPayload["request"]
941
+ | null;
942
+ const storedIntent = storedRequest
943
+ ? createDraftIntent(storedRequest)
944
+ : null;
945
+ const planIntent = createDraftIntent(plan.payload.request);
946
+ const legacyPreMarkerCreate = Boolean(
947
+ planIntent?.marker && storedIntent && !storedIntent.marker,
948
+ );
949
+ const validationStoredPayload = legacyPreMarkerCreate
950
+ ? {
951
+ ...storedPayload,
952
+ request: plan.payload.request,
953
+ }
954
+ : storedPayload;
955
+ const validatedPayload = validateBuilderCmsExecutionDryRun({
956
+ storedPayload: validationStoredPayload,
957
+ plan,
745
958
  now,
746
959
  });
747
- throw new Error(message);
748
- }
960
+ const dryRun = validatedPayload.dryRun;
961
+ if (dryRun?.status !== "validated") {
962
+ const message =
963
+ dryRun?.status === "stale" && dryRun.mismatches.length > 0
964
+ ? dryRun.mismatches.join(" ")
965
+ : (plan.lastError ?? "Builder execution is blocked.");
966
+ await deps.updateExecutionState({
967
+ executionId: execution.id,
968
+ state: "blocked",
969
+ summary: `${plan.summary} Execution blocked before write.`,
970
+ payload: validatedPayload,
971
+ lastError: message,
972
+ now,
973
+ });
974
+ throw new Error(message);
975
+ }
976
+ if (plan.state !== "ready") {
977
+ const message = plan.lastError ?? "Builder execution is not ready.";
978
+ await deps.updateExecutionState({
979
+ executionId: execution.id,
980
+ state: plan.state,
981
+ summary: plan.summary,
982
+ payload: validatedPayload,
983
+ lastError: message,
984
+ now,
985
+ });
986
+ throw new Error(message);
987
+ }
749
988
 
750
- if (
751
- source.capabilities.liveWritesEnabled !== true ||
752
- source.sourceTable !== BUILDER_CMS_SAFE_WRITE_MODEL
753
- ) {
754
- const message =
755
- source.capabilities.liveWritesEnabled === true
756
- ? `Live Builder writes are only allowed for ${BUILDER_CMS_SAFE_WRITE_MODEL}.`
757
- : "Live Builder writes are disabled for this source.";
758
- await deps.updateExecutionState({
989
+ if (source.capabilities.liveWritesEnabled !== true) {
990
+ const message = "Live Builder writes are disabled for this source.";
991
+ await deps.updateExecutionState({
992
+ executionId: execution.id,
993
+ state: "write_disabled",
994
+ summary: `${plan.summary} Execution blocked before write.`,
995
+ payload: validatedPayload,
996
+ lastError: message,
997
+ now,
998
+ });
999
+ throw new Error(message);
1000
+ }
1001
+
1002
+ let storedWriteResult = successfulStoredWriteResult(storedPayload);
1003
+ if (!storedWriteResult && planIntent && deps.lookupSafeModelIntent) {
1004
+ const lookup = await deps.lookupSafeModelIntent(
1005
+ legacyPreMarkerCreate
1006
+ ? {
1007
+ exactTitle: storedIntent?.exactTitle,
1008
+ intendedFields: storedIntent?.intendedFields,
1009
+ }
1010
+ : {
1011
+ marker: planIntent.marker,
1012
+ intendedFields: planIntent.intendedFields,
1013
+ },
1014
+ );
1015
+ const matchingIntentCount = lookup.matchingIntentCount ?? lookup.count;
1016
+ if (lookup.count > 1) {
1017
+ const lastError =
1018
+ "Builder reconciliation required: more than one remote draft matches this execution intent.";
1019
+ await deps.markExecutionFailed({
1020
+ executionId: execution.id,
1021
+ state: "reconciliation_required",
1022
+ summary: "Builder execution requires reconciliation.",
1023
+ payload: validatedPayload,
1024
+ lastError,
1025
+ now: deps.now(),
1026
+ });
1027
+ throw builderExecutionConflict(lastError);
1028
+ }
1029
+ if (lookup.count === 1 && matchingIntentCount !== 1) {
1030
+ const lastError = legacyPreMarkerCreate
1031
+ ? "Builder reconciliation required: an exact-title legacy candidate exists, but its intended fields do not match. Do not retry."
1032
+ : "Builder reconciliation required: the remote draft marker exists, but its intended fields drifted. Do not retry.";
1033
+ await deps.markExecutionFailed({
1034
+ executionId: execution.id,
1035
+ state: "reconciliation_required",
1036
+ summary: "Builder execution requires reconciliation.",
1037
+ payload: validatedPayload,
1038
+ lastError,
1039
+ now: deps.now(),
1040
+ });
1041
+ throw builderExecutionConflict(lastError);
1042
+ }
1043
+ if (lookup.count === 1) {
1044
+ storedWriteResult = recoveredWriteResult(lookup.matches[0]!);
1045
+ }
1046
+ }
1047
+ if (storedWriteResult) {
1048
+ timing.record("approval_gate_and_dry_run_validation", gateStartedAt);
1049
+ timing.ensure("write_dispatch");
1050
+ const reconciliationStartedAt = timing.start();
1051
+ const payloadWithResponse = executionResponsePayload({
1052
+ payload: validatedPayload,
1053
+ writeResult: storedWriteResult,
1054
+ });
1055
+ const reconciledAt = deps.now();
1056
+ try {
1057
+ await deps.reconcileWrite({
1058
+ database,
1059
+ source,
1060
+ changeSet,
1061
+ plan,
1062
+ writeResult: storedWriteResult,
1063
+ now: reconciledAt,
1064
+ });
1065
+ } catch (error) {
1066
+ timing.record(
1067
+ "reconciliation_and_persistence",
1068
+ reconciliationStartedAt,
1069
+ );
1070
+ const lastError = reconciliationErrorMessage(error);
1071
+ await deps.markExecutionFailed({
1072
+ executionId: execution.id,
1073
+ state: "reconciliation_required",
1074
+ summary: `Builder ${plan.pushMode} execution requires reconciliation.`,
1075
+ payload: payloadWithResponse,
1076
+ lastError,
1077
+ now: deps.now(),
1078
+ });
1079
+ throw builderExecutionConflict(lastError);
1080
+ }
1081
+ await deps.markExecutionSucceeded({
1082
+ executionId: execution.id,
1083
+ changeSetId: changeSet.id,
1084
+ summary: `Builder ${plan.pushMode} execution succeeded.`,
1085
+ payload: payloadWithResponse,
1086
+ now: reconciledAt,
1087
+ });
1088
+ const response = await deps.getResponse(database.id);
1089
+ timing.record("reconciliation_and_persistence", reconciliationStartedAt);
1090
+ const result = { ...response, timings: timing.finish() };
1091
+ timing.log("succeeded");
1092
+ return result;
1093
+ }
1094
+
1095
+ if (requiresLivePreflight(plan.payload.effect)) {
1096
+ const entryId = plan.payload.target.entryId;
1097
+ if (!entryId) {
1098
+ const message = "Builder entry no longer exists; refresh the source.";
1099
+ await deps.updateExecutionState({
1100
+ executionId: execution.id,
1101
+ state: "blocked",
1102
+ summary: `${plan.summary} Execution blocked before write.`,
1103
+ payload: validatedPayload,
1104
+ lastError: message,
1105
+ now,
1106
+ });
1107
+ throw new Error(message);
1108
+ }
1109
+
1110
+ const liveState = await deps.readLiveEntry({
1111
+ model: plan.payload.target.model,
1112
+ entryId,
1113
+ });
1114
+ const targetRow = sourceRowForChangeSet(source, changeSet);
1115
+ console.info("builder_source_live_preflight", {
1116
+ executionId: execution.id,
1117
+ changeSetId: changeSet.id,
1118
+ targetEntryId: entryId,
1119
+ baselineLastUpdated: targetRow?.lastSourceUpdatedAt ?? null,
1120
+ liveLastUpdated: liveState.lastUpdated,
1121
+ baselineBlocksHash: changeSet.bodyChange?.currentHash ?? null,
1122
+ liveBlocksHash: liveState.blocksHash,
1123
+ livePublished: liveState.published,
1124
+ });
1125
+ const message = livePreflightBlockMessage({
1126
+ liveState,
1127
+ baselineLastUpdated: targetRow?.lastSourceUpdatedAt ?? null,
1128
+ baselineBlocksHash: changeSet.bodyChange?.currentHash ?? null,
1129
+ effect: plan.payload.effect,
1130
+ });
1131
+ if (message) {
1132
+ await deps.updateExecutionState({
1133
+ executionId: execution.id,
1134
+ state: "blocked",
1135
+ summary: `${plan.summary} Execution blocked before write.`,
1136
+ payload: {
1137
+ ...validatedPayload,
1138
+ livePreflight: {
1139
+ checkedAt: now,
1140
+ exists: liveState.exists,
1141
+ published: liveState.published,
1142
+ lastUpdated: liveState.lastUpdated,
1143
+ blocksHash: liveState.blocksHash,
1144
+ id: liveState.id,
1145
+ },
1146
+ },
1147
+ lastError: message,
1148
+ now,
1149
+ });
1150
+ throw new Error(message);
1151
+ }
1152
+ }
1153
+
1154
+ const attemptToken = crypto.randomUUID();
1155
+ const claimed = await deps.claimExecution({
759
1156
  executionId: execution.id,
760
- state:
761
- source.capabilities.liveWritesEnabled === true
762
- ? "blocked"
763
- : "write_disabled",
764
- summary: `${plan.summary} Execution blocked before write.`,
1157
+ summary: `Running Builder ${plan.pushMode} execution.`,
765
1158
  payload: validatedPayload,
766
- lastError: message,
767
1159
  now,
1160
+ staleBefore: staleRunningCutoff(now),
1161
+ attemptToken,
768
1162
  });
769
- throw new Error(message);
770
- }
1163
+ if (!claimed) {
1164
+ throw builderExecutionConflict("Builder execution is already running.");
1165
+ }
1166
+ timing.record("approval_gate_and_dry_run_validation", gateStartedAt);
771
1167
 
772
- const storedWriteResult = successfulStoredWriteResult(storedPayload);
773
- if (storedWriteResult) {
1168
+ const writeResult = await timing.measure("write_dispatch", () =>
1169
+ deps.executeWrite({ request: plan.payload.request }),
1170
+ );
774
1171
  const payloadWithResponse = executionResponsePayload({
775
1172
  payload: validatedPayload,
776
- writeResult: storedWriteResult,
1173
+ writeResult,
777
1174
  });
778
- const reconciledAt = deps.now();
1175
+
1176
+ if (!writeResult.ok) {
1177
+ const lastError =
1178
+ writeResult.error ??
1179
+ `Builder write request failed with HTTP ${writeResult.status}.`;
1180
+ await deps.markExecutionFailed({
1181
+ executionId: execution.id,
1182
+ state: writeResult.ambiguity ? "reconciliation_required" : "failed",
1183
+ summary: writeResult.ambiguity
1184
+ ? `Builder ${plan.pushMode} execution requires reconciliation.`
1185
+ : `Builder ${plan.pushMode} execution failed.`,
1186
+ payload: payloadWithResponse,
1187
+ lastError,
1188
+ now: deps.now(),
1189
+ attemptToken,
1190
+ });
1191
+ throw writeResult.ambiguity
1192
+ ? builderExecutionConflict(lastError)
1193
+ : new Error(lastError);
1194
+ }
1195
+
1196
+ const reconciliationStartedAt = timing.start();
1197
+ if (deps.checkpointResponse) {
1198
+ const checkpointed = await deps.checkpointResponse({
1199
+ executionId: execution.id,
1200
+ attemptToken,
1201
+ payload: payloadWithResponse,
1202
+ now: deps.now(),
1203
+ });
1204
+ if (!checkpointed) {
1205
+ throw new Error(
1206
+ "Execution lease was reclaimed before response checkpoint.",
1207
+ );
1208
+ }
1209
+ } else {
1210
+ await deps.updateExecutionState({
1211
+ executionId: execution.id,
1212
+ state: "response_received",
1213
+ summary: "Builder response received; reconciling locally.",
1214
+ payload: payloadWithResponse,
1215
+ lastError: null,
1216
+ now: deps.now(),
1217
+ });
1218
+ }
1219
+
1220
+ const succeededAt = deps.now();
779
1221
  try {
780
1222
  await deps.reconcileWrite({
781
1223
  database,
782
1224
  source,
783
1225
  changeSet,
784
1226
  plan,
785
- writeResult: storedWriteResult,
786
- now: reconciledAt,
1227
+ writeResult,
1228
+ now: succeededAt,
787
1229
  });
788
1230
  } catch (error) {
1231
+ timing.record("reconciliation_and_persistence", reconciliationStartedAt);
789
1232
  const lastError = reconciliationErrorMessage(error);
790
1233
  await deps.markExecutionFailed({
791
1234
  executionId: execution.id,
792
- summary: `Builder ${plan.pushMode} execution reconciliation failed.`,
1235
+ state: "reconciliation_required",
1236
+ summary: `Builder ${plan.pushMode} execution requires reconciliation.`,
793
1237
  payload: payloadWithResponse,
794
1238
  lastError,
795
1239
  now: deps.now(),
1240
+ attemptToken,
796
1241
  });
797
- throw new Error(lastError);
1242
+ throw builderExecutionConflict(lastError);
798
1243
  }
799
1244
  await deps.markExecutionSucceeded({
800
1245
  executionId: execution.id,
801
1246
  changeSetId: changeSet.id,
802
1247
  summary: `Builder ${plan.pushMode} execution succeeded.`,
803
1248
  payload: payloadWithResponse,
804
- now: reconciledAt,
805
- });
806
- return deps.getResponse(database.id);
807
- }
808
-
809
- if (requiresLivePreflight(plan.payload.effect)) {
810
- const entryId = plan.payload.target.entryId;
811
- if (!entryId) {
812
- const message = "Builder entry no longer exists; refresh the source.";
813
- await deps.updateExecutionState({
814
- executionId: execution.id,
815
- state: "blocked",
816
- summary: `${plan.summary} Execution blocked before write.`,
817
- payload: validatedPayload,
818
- lastError: message,
819
- now,
820
- });
821
- throw new Error(message);
822
- }
823
-
824
- const liveState = await deps.readLiveEntry({
825
- model: plan.payload.target.model,
826
- entryId,
827
- });
828
- const targetRow = sourceRowForChangeSet(source, changeSet);
829
- const message = livePreflightBlockMessage({
830
- liveState,
831
- baselineLastUpdated: targetRow?.lastSourceUpdatedAt ?? null,
832
- baselineBlocksHash: changeSet.bodyChange?.currentHash ?? null,
833
- effect: plan.payload.effect,
834
- });
835
- if (message) {
836
- await deps.updateExecutionState({
837
- executionId: execution.id,
838
- state: "blocked",
839
- summary: `${plan.summary} Execution blocked before write.`,
840
- payload: {
841
- ...validatedPayload,
842
- livePreflight: {
843
- checkedAt: now,
844
- exists: liveState.exists,
845
- published: liveState.published,
846
- lastUpdated: liveState.lastUpdated,
847
- blocksHash: liveState.blocksHash,
848
- id: liveState.id,
849
- },
850
- },
851
- lastError: message,
852
- now,
853
- });
854
- throw new Error(message);
855
- }
856
- }
857
-
858
- const claimed = await deps.claimExecution({
859
- executionId: execution.id,
860
- summary: `Running Builder ${plan.pushMode} execution.`,
861
- payload: validatedPayload,
862
- now,
863
- staleBefore: staleRunningCutoff(now),
864
- });
865
- if (!claimed) {
866
- throw new Error("Builder execution is already running.");
867
- }
868
-
869
- const writeResult = await deps.executeWrite({
870
- request: plan.payload.request,
871
- });
872
- const payloadWithResponse = executionResponsePayload({
873
- payload: validatedPayload,
874
- writeResult,
875
- });
876
-
877
- if (!writeResult.ok) {
878
- const lastError =
879
- writeResult.error ??
880
- `Builder write request failed with HTTP ${writeResult.status}.`;
881
- await deps.markExecutionFailed({
882
- executionId: execution.id,
883
- summary: `Builder ${plan.pushMode} execution failed.`,
884
- payload: payloadWithResponse,
885
- lastError,
886
- now: deps.now(),
887
- });
888
- throw new Error(lastError);
889
- }
890
-
891
- const succeededAt = deps.now();
892
- try {
893
- await deps.reconcileWrite({
894
- database,
895
- source,
896
- changeSet,
897
- plan,
898
- writeResult,
899
1249
  now: succeededAt,
1250
+ attemptToken,
900
1251
  });
1252
+
1253
+ const response = await deps.getResponse(database.id);
1254
+ timing.record("reconciliation_and_persistence", reconciliationStartedAt);
1255
+ const result = { ...response, timings: timing.finish() };
1256
+ timing.log("succeeded");
1257
+ return result;
901
1258
  } catch (error) {
902
- const lastError = reconciliationErrorMessage(error);
903
- await deps.markExecutionFailed({
904
- executionId: execution.id,
905
- summary: `Builder ${plan.pushMode} execution reconciliation failed.`,
906
- payload: payloadWithResponse,
907
- lastError,
908
- now: deps.now(),
909
- });
910
- throw new Error(lastError);
1259
+ timing.ensure("approval_gate_and_dry_run_validation");
1260
+ timing.ensure("write_dispatch");
1261
+ timing.ensure("reconciliation_and_persistence");
1262
+ timing.log("failed");
1263
+ throw error;
911
1264
  }
912
- await deps.markExecutionSucceeded({
913
- executionId: execution.id,
914
- changeSetId: changeSet.id,
915
- summary: `Builder ${plan.pushMode} execution succeeded.`,
916
- payload: payloadWithResponse,
917
- now: succeededAt,
918
- });
919
-
920
- return deps.getResponse(database.id);
921
1265
  }
922
1266
 
923
1267
  export default defineAction({
924
1268
  description:
925
- "Execute a prepared Builder CMS write gate. This performs a real Builder write only when the approved outbound change-set, push mode, source capability, safe test model, and idempotency gates all pass.",
1269
+ "Execute a prepared Builder CMS write gate. This performs a real Builder write only when the approved outbound change-set, push mode, per-source capability, validation, publication, and idempotency gates all pass.",
926
1270
  schema: z.object({
927
1271
  databaseId: z.string().optional().describe("Database ID"),
928
1272
  documentId: z.string().optional().describe("Database document/page ID"),