@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +28 -0
- package/corpus/core/docs/content/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +9 -7
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/artifact-response.ts +255 -12
- package/corpus/core/src/agent/thread-data-builder.ts +84 -11
- package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
- package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
- package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
- package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
- package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +99 -7
- package/corpus/core/src/integrations/adapters/discord.ts +3 -1
- package/corpus/core/src/integrations/adapters/email.ts +4 -1
- package/corpus/core/src/integrations/adapters/google-docs.ts +4 -1
- package/corpus/core/src/integrations/adapters/microsoft-teams.ts +3 -1
- package/corpus/core/src/integrations/adapters/slack.ts +38 -9
- package/corpus/core/src/integrations/adapters/telegram.ts +32 -10
- package/corpus/core/src/integrations/adapters/whatsapp.ts +5 -1
- package/corpus/core/src/integrations/index.ts +1 -0
- package/corpus/core/src/integrations/types.ts +9 -2
- package/corpus/core/src/integrations/webhook-handler.ts +69 -45
- package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +1 -0
- package/corpus/core/src/server/auth.ts +25 -1
- package/corpus/core/src/server/builder-browser.ts +353 -1
- package/corpus/core/src/server/core-routes-plugin.ts +601 -223
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
- package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
- package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
- package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
- package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
- package/corpus/templates/content/.agents/skills/content/SKILL.md +31 -0
- package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +35 -21
- package/corpus/templates/content/AGENTS.md +32 -9
- package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
- package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
- package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
- package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
- package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +1071 -170
- package/corpus/templates/content/actions/_database-utils.ts +104 -13
- package/corpus/templates/content/actions/_local-file-documents.ts +53 -1
- package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
- package/corpus/templates/content/actions/_property-utils.ts +7 -1
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
- package/corpus/templates/content/actions/configure-document-property.ts +11 -0
- package/corpus/templates/content/actions/create-content-database.ts +11 -0
- package/corpus/templates/content/actions/create-document.ts +9 -0
- package/corpus/templates/content/actions/create-inline-content-database.ts +3 -0
- package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
- package/corpus/templates/content/actions/export-content-source.ts +1 -0
- package/corpus/templates/content/actions/get-content-database.ts +13 -1
- package/corpus/templates/content/actions/get-document.ts +12 -2
- package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
- package/corpus/templates/content/actions/import-content-source.ts +6 -0
- package/corpus/templates/content/actions/list-documents.ts +3 -0
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
- package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
- package/corpus/templates/content/actions/pull-document.ts +2 -0
- package/corpus/templates/content/actions/search-documents.ts +5 -1
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
- package/corpus/templates/content/actions/update-document.ts +16 -1
- package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
- package/corpus/templates/content/actions/view-screen.ts +15 -2
- package/corpus/templates/content/app/components/editor/DescriptionField.tsx +121 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +14 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +250 -49
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
- package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2414 -589
- package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
- package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
- package/corpus/templates/content/app/global.css +18 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
- package/corpus/templates/content/app/i18n/zh-TW.ts +30 -0
- package/corpus/templates/content/app/i18n-data.ts +339 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-pages-and-databases-can-describe-themselves-with-guidance-fo.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-slack-follow-ups-now-retain-created-content-identity-and-per.md +6 -0
- package/corpus/templates/content/parity/matrix.md +27 -25
- package/corpus/templates/content/parity/matrix.ts +50 -2
- package/corpus/templates/content/server/db/schema.ts +51 -0
- package/corpus/templates/content/server/lib/document-context.ts +76 -0
- package/corpus/templates/content/server/lib/public-documents.ts +3 -0
- package/corpus/templates/content/server/plugins/auth.ts +1 -0
- package/corpus/templates/content/server/plugins/db.ts +59 -0
- package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +5 -0
- package/corpus/templates/content/shared/api.ts +87 -1
- package/corpus/templates/content/shared/builder-mdx.ts +705 -15
- package/corpus/templates/content/shared/content-source.ts +7 -0
- package/corpus/templates/content/shared/nfm.ts +3 -3
- package/corpus/templates/content/shared/properties.ts +2 -0
- package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
- package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
- package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/design/app/i18n/en-US.ts +1 -0
- package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
- package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
- package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
- package/corpus/templates/design/shared/review-anchor.ts +48 -0
- package/corpus/templates/plan/server/plan-mdx.ts +53 -7
- package/dist/a2a/artifact-response.d.ts +15 -0
- package/dist/a2a/artifact-response.d.ts.map +1 -1
- package/dist/a2a/artifact-response.js +204 -5
- package/dist/a2a/artifact-response.js.map +1 -1
- package/dist/agent/thread-data-builder.d.ts +6 -0
- package/dist/agent/thread-data-builder.d.ts.map +1 -1
- package/dist/agent/thread-data-builder.js +71 -8
- package/dist/agent/thread-data-builder.js.map +1 -1
- package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
- package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
- package/dist/client/review/ReviewCommentComposer.js +3 -3
- package/dist/client/review/ReviewCommentComposer.js.map +1 -1
- package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
- package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
- package/dist/client/review/ReviewThreadPanel.js +6 -4
- package/dist/client/review/ReviewThreadPanel.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts +3 -0
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +11 -4
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +8 -4
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +19 -6
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
- package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +72 -6
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/integrations/adapters/discord.d.ts.map +1 -1
- package/dist/integrations/adapters/discord.js +1 -0
- package/dist/integrations/adapters/discord.js.map +1 -1
- package/dist/integrations/adapters/email.d.ts.map +1 -1
- package/dist/integrations/adapters/email.js +2 -0
- package/dist/integrations/adapters/email.js.map +1 -1
- package/dist/integrations/adapters/google-docs.d.ts.map +1 -1
- package/dist/integrations/adapters/google-docs.js +2 -0
- package/dist/integrations/adapters/google-docs.js.map +1 -1
- package/dist/integrations/adapters/microsoft-teams.d.ts.map +1 -1
- package/dist/integrations/adapters/microsoft-teams.js +1 -0
- package/dist/integrations/adapters/microsoft-teams.js.map +1 -1
- package/dist/integrations/adapters/slack.d.ts.map +1 -1
- package/dist/integrations/adapters/slack.js +21 -6
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/adapters/telegram.d.ts.map +1 -1
- package/dist/integrations/adapters/telegram.js +11 -2
- package/dist/integrations/adapters/telegram.js.map +1 -1
- package/dist/integrations/adapters/whatsapp.d.ts.map +1 -1
- package/dist/integrations/adapters/whatsapp.js +3 -0
- package/dist/integrations/adapters/whatsapp.js.map +1 -1
- package/dist/integrations/index.d.ts +1 -1
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/types.d.ts +8 -2
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +40 -43
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +1 -0
- package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
- package/dist/server/auth.d.ts +1 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +19 -2
- package/dist/server/auth.js.map +1 -1
- package/dist/server/builder-browser.d.ts +78 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +246 -1
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +41 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +260 -14
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/docs/content/deployment.mdx +9 -7
- package/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/docs/content/locales/zh-TW/deployment.mdx +9 -7
- 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 (
|
|
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 [
|
|
539
|
-
.select(
|
|
540
|
-
|
|
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.
|
|
546
|
-
args.
|
|
658
|
+
schema.contentDatabaseSourceExecutionClaims.ownerEmail,
|
|
659
|
+
args.ownerEmail,
|
|
547
660
|
),
|
|
548
661
|
eq(
|
|
549
|
-
schema.
|
|
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
|
-
|
|
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
|
-
|
|
603
|
-
const
|
|
604
|
-
await db
|
|
738
|
+
checkpointResponse: async (args) => {
|
|
739
|
+
const result = await getDb()
|
|
605
740
|
.update(schema.contentDatabaseSourceExecutions)
|
|
606
741
|
.set({
|
|
607
|
-
state: "
|
|
608
|
-
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(
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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(
|
|
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
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
660
|
-
source
|
|
661
|
-
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
927
|
+
if (changeSet.state !== "approved") {
|
|
928
|
+
throw new Error("Approve the Builder change set before executing it.");
|
|
929
|
+
}
|
|
707
930
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
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
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
:
|
|
758
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
773
|
-
|
|
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
|
|
1173
|
+
writeResult,
|
|
777
1174
|
});
|
|
778
|
-
|
|
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
|
|
786
|
-
now:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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,
|
|
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"),
|