@agent-native/core-corpus 0.216.0 → 0.218.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +61 -1
- package/corpus/templates/analytics/AGENTS.md +1 -1
- package/corpus/templates/analytics/CHANGELOG.md +3 -0
- package/corpus/templates/analytics/actions/get-session-replay-summary.ts +9 -65
- package/corpus/templates/analytics/actions/list-event-catalog.ts +1 -1
- package/corpus/templates/analytics/actions/list-session-outputs.ts +40 -0
- package/corpus/templates/analytics/actions/list-session-recordings.ts +17 -4
- package/corpus/templates/analytics/actions/view-screen.ts +29 -3
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +38 -167
- package/corpus/templates/analytics/app/i18n/en-US.ts +27 -0
- package/corpus/templates/analytics/app/pages/sessions/EventCatalogPage.tsx +18 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +3 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionOutputs.tsx +197 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionsTriagePage.tsx +114 -4
- package/corpus/templates/analytics/changelog/2026-10-15-sessions-can-find-people-who-made-a-slides-deck-but-didn-t-p.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-the-activation-funnel-counts-visitors-who-decline-analytics-.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +67 -0
- package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +9 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +28 -22
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +36 -1
- package/corpus/templates/analytics/server/lib/output-outcomes.ts +712 -0
- package/corpus/templates/analytics/server/lib/replay-origins.ts +33 -0
- package/corpus/templates/analytics/server/lib/session-event-index.ts +20 -0
- package/corpus/templates/analytics/server/lib/session-performance.ts +6 -40
- package/corpus/templates/analytics/server/lib/session-recording-associations.ts +30 -1
- package/corpus/templates/analytics/server/lib/session-replay-triage.ts +99 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +45 -31
- package/corpus/templates/analytics/server/lib/sessions-triage-lab.ts +8 -2
- package/corpus/templates/analytics/server/plugins/db.ts +46 -0
- package/corpus/templates/analytics/shared/output-use.ts +205 -0
- package/corpus/templates/analytics/shared/session-events.ts +6 -0
- package/corpus/templates/assets/CHANGELOG.md +6 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +51 -117
- package/corpus/templates/assets/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +34 -101
- package/corpus/templates/brain/app/routes/sources.tsx +1 -1
- package/corpus/templates/brain/vitest.config.ts +7 -0
- package/corpus/templates/calendar/CHANGELOG.md +12 -0
- package/corpus/templates/calendar/actions/create-event.ts +2 -4
- package/corpus/templates/calendar/actions/update-event.ts +35 -6
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +1 -1
- package/corpus/templates/calendar/changelog/2026-10-16-booking-links-show-localized-visibility-badges.md +6 -0
- package/corpus/templates/calendar/changelog/2026-10-21-solo-events-avoid-notification-approval.md +6 -0
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +62 -101
- package/corpus/templates/clips/CHANGELOG.md +7 -20
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +20 -77
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +63 -90
- package/corpus/templates/clips/app/components/sharing/share-test-support.tsx +180 -0
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -584
- package/corpus/templates/clips/changelog/2026-10-16-share-dialogs-make-it-easier-to-see-who-has-access-and-manag.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-21-share-panels-describe-general-access-in-a-sentence.md +6 -0
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -3
- package/corpus/templates/content/CHANGELOG.md +10 -0
- package/corpus/templates/content/actions/_browser-document-save-attempt.ts +23 -1
- package/corpus/templates/content/actions/_builder-cms-blob-custody.ts +467 -0
- package/corpus/templates/content/actions/_builder-cms-execution-lease.ts +17 -0
- package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +5 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +881 -21
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +28 -2
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +109 -11
- package/corpus/templates/content/actions/_builder-docs-client.ts +1 -0
- package/corpus/templates/content/actions/_builder-private-blob.test-fixture.ts +90 -0
- package/corpus/templates/content/actions/_builder-source-execution-guard.ts +48 -0
- package/corpus/templates/content/actions/_content-save-outcomes.ts +5 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +290 -8
- package/corpus/templates/content/actions/_database-utils.ts +93 -7
- package/corpus/templates/content/actions/_database-view-setup.ts +21 -2
- package/corpus/templates/content/actions/_delete-content-space.ts +8 -1
- package/corpus/templates/content/actions/_github-source-freshness.ts +30 -4
- package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -6
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +110 -26
- package/corpus/templates/content/actions/change-content-database-source-role.ts +51 -7
- package/corpus/templates/content/actions/delete-document.ts +41 -3
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +68 -2
- package/corpus/templates/content/actions/disconnect-local-folder-source.ts +33 -2
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +900 -140
- package/corpus/templates/content/actions/get-document.ts +2 -0
- package/corpus/templates/content/actions/permanently-delete-document.ts +6 -0
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +155 -13
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +299 -73
- package/corpus/templates/content/actions/set-live-body-switch.ts +45 -0
- package/corpus/templates/content/actions/update-document.ts +139 -20
- package/corpus/templates/content/actions/validate-builder-source-execution.ts +135 -26
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +119 -30
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +56 -13
- package/corpus/templates/content/app/components/editor/SuggestionText.tsx +18 -3
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +89 -22
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +23 -0
- package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +27 -2
- package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +32 -8
- package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +27 -2
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +1 -0
- package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +383 -34
- package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +31 -0
- package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +82 -4
- package/corpus/templates/content/app/global.css +31 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +40 -2
- package/corpus/templates/content/app/hooks/use-documents.ts +14 -0
- package/corpus/templates/content/app/i18n-data.ts +1 -0
- package/corpus/templates/content/changelog/2026-10-16-accepting-a-suggestion-on-a-page-that-changed-since-it-was-s.md +6 -0
- package/corpus/templates/content/changelog/2026-10-17-builder-content-avoids-retrying-a-write-when-builder-accepte.md +6 -0
- package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +1 -1
- package/corpus/templates/content/changelog/2026-10-20-builder-content-sync-keeps-read-and-write-status-accurate-wh.md +6 -0
- package/corpus/templates/content/changelog/2026-10-20-suggested-edits-that-add-remove-split-or-merge-paragraphs-li.md +6 -0
- package/corpus/templates/content/changelog/2026-10-21-list-rows-can-show-each-pages-first-lines.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-a-collection-linked-to-a-big-github-folder-fills-faster-and-p.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.adapters.md +29 -0
- package/corpus/templates/content/parity/matrix.md +42 -42
- package/corpus/templates/content/parity/matrix.ts +3 -1
- package/corpus/templates/content/server/db/schema.ts +20 -0
- package/corpus/templates/content/server/lib/content-trash-purge.ts +15 -1
- package/corpus/templates/content/server/lib/live-body-writer.ts +138 -26
- package/corpus/templates/content/server/live-copy/binding.ts +31 -0
- package/corpus/templates/content/server/live-copy/bounded-task.d.ts +6 -0
- package/corpus/templates/content/server/live-copy/exec-db.ts +1 -0
- package/corpus/templates/content/server/live-copy/holds.ts +59 -0
- package/corpus/templates/content/server/live-copy/persist.ts +656 -0
- package/corpus/templates/content/server/live-copy/switch.ts +136 -0
- package/corpus/templates/content/server/live-copy/task-names.ts +4 -0
- package/corpus/templates/content/server/live-copy/tasks.ts +183 -0
- package/corpus/templates/content/server/live-copy/write-cause.ts +85 -0
- package/corpus/templates/content/server/live-copy/write.ts +85 -0
- package/corpus/templates/content/server/plugins/builder-private-payload-cleanup.ts +22 -0
- package/corpus/templates/content/server/plugins/collab.ts +3 -0
- package/corpus/templates/content/server/plugins/db.ts +28 -0
- package/corpus/templates/content/shared/api.ts +30 -1
- package/corpus/templates/content/shared/database-item-layout.ts +54 -22
- package/corpus/templates/content/shared/document-intent-merge.ts +16 -9
- package/corpus/templates/content/shared/og-description.ts +258 -36
- package/corpus/templates/content/shared/suggestion-diff.ts +23 -6
- package/corpus/templates/content/shared/suggestion-formatting.ts +44 -14
- package/corpus/templates/content/shared/suggestion-text.ts +20 -5
- package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +5 -1
- package/corpus/templates/design/.generated/bridge/shader-runtime.generated.ts +25 -4
- package/corpus/templates/design/CHANGELOG.md +5 -0
- package/corpus/templates/design/actions/get-getting-started.ts +53 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +10 -0
- package/corpus/templates/design/app/components/design/bridge/native-shader-runtime.bridge.ts +132 -79
- package/corpus/templates/design/app/components/design/bridge/native-source-provider.ts +268 -130
- package/corpus/templates/design/app/components/design/bridge/native-worker-scene-host.ts +122 -42
- package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +41 -4
- package/corpus/templates/design/app/components/design/design-canvas/text-content-change-result.ts +46 -12
- package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +15 -8
- package/corpus/templates/design/app/components/layout/GettingStarted.tsx +50 -0
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +3 -0
- package/corpus/templates/design/app/components/templates/DesignTemplateGrid.tsx +163 -0
- package/corpus/templates/design/app/components/templates/DesignTemplateLibrary.tsx +65 -78
- package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +6 -2
- package/corpus/templates/design/app/global.css +6 -0
- package/corpus/templates/design/app/i18n/en-US.ts +17 -0
- package/corpus/templates/design/app/lib/canvas-load-outcome.ts +147 -0
- package/corpus/templates/design/app/lib/canvas-outcome-tracking.ts +531 -0
- package/corpus/templates/design/app/lib/canvas-style-kind.ts +50 -0
- package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
- package/corpus/templates/design/app/pages/Index.tsx +28 -67
- package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +86 -29
- package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +38 -3
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-core.ts +9 -0
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-files-and-saving.ts +2 -0
- package/corpus/templates/design/changelog/2026-10-16-design-pages-use-localized-visibility-badges-and-a-shared-ow.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-a-getting-started-checklist-at-the-bottom-of-the-sidebar-tra.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-templates-now-open-a-read-only-preview-from-the-card-and-the.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-collapsed-shader-layers-no-longer-interrupt-other-shaders-on.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-shader-fills-keep-rendering-when-other-screen-content-cannot.md +6 -0
- package/corpus/templates/design/scripts/design-export-validation/export-baseline.json +24 -0
- package/corpus/templates/design/server/lib/getting-started.ts +267 -0
- package/corpus/templates/design/server/plugins/getting-started.ts +5 -0
- package/corpus/templates/design/shared/design-template-presets/analytics-dashboard.ts +127 -0
- package/corpus/templates/design/shared/design-template-presets/design-studio.ts +98 -0
- package/corpus/templates/design/shared/design-template-presets/messaging-app.ts +101 -0
- package/corpus/templates/design/shared/design-template-presets/project-tasks.ts +120 -0
- package/corpus/templates/design/shared/design-template-presets.ts +8 -0
- package/corpus/templates/dispatch/CHANGELOG.md +4 -0
- package/corpus/templates/dispatch/app/i18n-data.ts +75 -0
- package/corpus/templates/dispatch/changelog/2026-10-21-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/factory/app/components/layout/Sidebar.tsx +34 -100
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +1 -1
- package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/plan/CHANGELOG.md +6 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +34 -109
- package/corpus/templates/plan/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/slides/CHANGELOG.md +7 -0
- package/corpus/templates/slides/actions/delete-deck.ts +7 -0
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +1 -1
- package/corpus/templates/slides/app/pages/Index.tsx +7 -2
- package/corpus/templates/slides/changelog/2026-10-16-deck-lists-use-the-shared-ownership-filter-and-visibility-ba.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-21-connected-deck-deletion-requires-approval.md +6 -0
- package/corpus/templates/slides/scripts/edit-fidelity/client-action.ts +61 -0
- package/corpus/templates/slides/scripts/edit-fidelity/run.ts +10 -0
- package/package.json +1 -1
- package/corpus/templates/chat/app/lib/sidebar-thread-state.ts +0 -29
- package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +0 -83
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
defineAction,
|
|
3
|
+
fail,
|
|
4
|
+
isActionContractError,
|
|
5
|
+
} from "@agent-native/core/action";
|
|
2
6
|
import { assertAccess } from "@agent-native/core/sharing";
|
|
3
|
-
import { and, eq, lt, notInArray, or } from "drizzle-orm";
|
|
7
|
+
import { and, eq, exists, isNull, lt, notInArray, or } from "drizzle-orm";
|
|
4
8
|
import { z } from "zod";
|
|
5
9
|
|
|
6
10
|
import { getDb, schema } from "../server/db/index.js";
|
|
@@ -14,6 +18,17 @@ import {
|
|
|
14
18
|
type DocumentPropertyValue,
|
|
15
19
|
type ExecuteBuilderSourceExecutionRequest,
|
|
16
20
|
} from "../shared/api.js";
|
|
21
|
+
import {
|
|
22
|
+
builderExecutionPayloadReference,
|
|
23
|
+
cleanupBuilderPrivatePayload,
|
|
24
|
+
enqueueBuilderPrivatePayloadCleanup,
|
|
25
|
+
readBuilderExecutionPayload,
|
|
26
|
+
storeBuilderExecutionPayload,
|
|
27
|
+
} from "./_builder-cms-blob-custody.js";
|
|
28
|
+
import {
|
|
29
|
+
builderExecutionStaleBefore,
|
|
30
|
+
isBuilderExecutionClaimStale,
|
|
31
|
+
} from "./_builder-cms-execution-lease.js";
|
|
17
32
|
import {
|
|
18
33
|
lookupBuilderCmsSafeModelIntent,
|
|
19
34
|
type BuilderCmsIntentMatch,
|
|
@@ -44,6 +59,7 @@ import {
|
|
|
44
59
|
executeBuilderCmsWrite,
|
|
45
60
|
} from "./_builder-cms-write-client.js";
|
|
46
61
|
import { createBuilderSourceTiming } from "./_builder-source-timings.js";
|
|
62
|
+
import { lockContentDatabaseMutation } from "./_content-database-mutation-lock.js";
|
|
47
63
|
import { lockDatabaseMemberships } from "./_database-membership-lock.js";
|
|
48
64
|
import {
|
|
49
65
|
getContentDatabaseSourceSnapshotForWrite,
|
|
@@ -60,6 +76,7 @@ export interface BuilderSourceExecutionRecord {
|
|
|
60
76
|
state: ContentDatabaseSourceExecutionState | (string & {});
|
|
61
77
|
idempotencyKey: string;
|
|
62
78
|
payloadJson: string;
|
|
79
|
+
lastError?: string | null;
|
|
63
80
|
attemptToken?: string | null;
|
|
64
81
|
updatedAt: string;
|
|
65
82
|
}
|
|
@@ -84,6 +101,10 @@ export interface ExecuteBuilderSourceExecutionDeps {
|
|
|
84
101
|
}) => Promise<BuilderSourceExecutionRecord | null>;
|
|
85
102
|
updateExecutionState: (args: {
|
|
86
103
|
executionId: string;
|
|
104
|
+
expected: Pick<
|
|
105
|
+
BuilderSourceExecutionRecord,
|
|
106
|
+
"state" | "updatedAt" | "attemptToken"
|
|
107
|
+
>;
|
|
87
108
|
state: ContentDatabaseSourceExecutionState;
|
|
88
109
|
summary: string;
|
|
89
110
|
payload: unknown;
|
|
@@ -92,18 +113,35 @@ export interface ExecuteBuilderSourceExecutionDeps {
|
|
|
92
113
|
}) => Promise<void>;
|
|
93
114
|
claimExecution: (args: {
|
|
94
115
|
executionId: string;
|
|
116
|
+
changeSetId: string;
|
|
117
|
+
/** The row the gate validated; any later write voids the claim. */
|
|
118
|
+
expected: Pick<
|
|
119
|
+
BuilderSourceExecutionRecord,
|
|
120
|
+
"state" | "updatedAt" | "attemptToken"
|
|
121
|
+
>;
|
|
95
122
|
summary: string;
|
|
96
123
|
payload: unknown;
|
|
97
124
|
now: string;
|
|
98
125
|
staleBefore: string;
|
|
99
126
|
attemptToken: string;
|
|
100
127
|
}) => Promise<boolean>;
|
|
128
|
+
renewExecutionClaim: (args: {
|
|
129
|
+
executionId: string;
|
|
130
|
+
attemptToken: string;
|
|
131
|
+
now: string;
|
|
132
|
+
}) => Promise<boolean>;
|
|
101
133
|
checkpointResponse?: (args: {
|
|
102
134
|
executionId: string;
|
|
103
135
|
attemptToken: string;
|
|
104
136
|
payload: unknown;
|
|
105
137
|
now: string;
|
|
106
138
|
}) => Promise<boolean>;
|
|
139
|
+
markResponseCheckpointFailed: (args: {
|
|
140
|
+
executionId: string;
|
|
141
|
+
attemptToken: string;
|
|
142
|
+
lastError: string;
|
|
143
|
+
now: string;
|
|
144
|
+
}) => Promise<boolean>;
|
|
107
145
|
markExecutionSucceeded: (args: {
|
|
108
146
|
executionId: string;
|
|
109
147
|
changeSetId: string;
|
|
@@ -121,12 +159,29 @@ export interface ExecuteBuilderSourceExecutionDeps {
|
|
|
121
159
|
attemptToken?: string;
|
|
122
160
|
state?: "failed" | "reconciliation_required";
|
|
123
161
|
}) => Promise<void>;
|
|
162
|
+
releaseExecutionClaim: (args: {
|
|
163
|
+
executionId: string;
|
|
164
|
+
attemptToken: string;
|
|
165
|
+
restore: Pick<
|
|
166
|
+
BuilderSourceExecutionRecord,
|
|
167
|
+
"state" | "attemptToken" | "updatedAt"
|
|
168
|
+
>;
|
|
169
|
+
summary: string;
|
|
170
|
+
payload: unknown;
|
|
171
|
+
lastError: string;
|
|
172
|
+
now: string;
|
|
173
|
+
}) => Promise<void>;
|
|
124
174
|
executeWrite: (args: {
|
|
125
175
|
request: BuilderCmsExecutionPayload["request"];
|
|
176
|
+
expectedSourceSpace?: string | null;
|
|
177
|
+
expectedSourceConnectionId?: string | null;
|
|
178
|
+
requireSourceBinding?: boolean;
|
|
126
179
|
}) => ReturnType<typeof executeBuilderCmsWrite>;
|
|
127
180
|
readLiveEntry: (args: {
|
|
128
181
|
model: string;
|
|
129
182
|
entryId: string;
|
|
183
|
+
expectedSourceSpace?: string | null;
|
|
184
|
+
expectedSourceConnectionId?: string | null;
|
|
130
185
|
}) => Promise<BuilderCmsEntryLiveState>;
|
|
131
186
|
reconcileWrite: (args: {
|
|
132
187
|
database: DatabaseRecord;
|
|
@@ -141,6 +196,8 @@ export interface ExecuteBuilderSourceExecutionDeps {
|
|
|
141
196
|
marker?: string;
|
|
142
197
|
exactTitle?: string;
|
|
143
198
|
intendedFields?: Record<string, unknown>;
|
|
199
|
+
expectedSourceSpace?: string | null;
|
|
200
|
+
expectedSourceConnectionId?: string | null;
|
|
144
201
|
}) => Promise<{
|
|
145
202
|
count: number;
|
|
146
203
|
matchingIntentCount?: number;
|
|
@@ -148,7 +205,9 @@ export interface ExecuteBuilderSourceExecutionDeps {
|
|
|
148
205
|
}>;
|
|
149
206
|
}
|
|
150
207
|
|
|
151
|
-
const
|
|
208
|
+
const BUILDER_LIVE_PREFLIGHT_TIMEOUT_MS = 60 * 1000;
|
|
209
|
+
const BUILDER_RESPONSE_CHECKPOINT_FAILURE =
|
|
210
|
+
"Builder accepted the write, but its response could not be saved for reconciliation. Do not retry this execution; inspect the live Builder entry before preparing another write.";
|
|
152
211
|
|
|
153
212
|
function parsePayload(value: string) {
|
|
154
213
|
try {
|
|
@@ -203,14 +262,11 @@ function executablePushMode(
|
|
|
203
262
|
}
|
|
204
263
|
|
|
205
264
|
function staleRunningCutoff(now: string) {
|
|
206
|
-
|
|
207
|
-
return new Date(
|
|
208
|
-
(Number.isFinite(timestamp) ? timestamp : Date.now()) -
|
|
209
|
-
RUNNING_EXECUTION_TIMEOUT_MS,
|
|
210
|
-
).toISOString();
|
|
265
|
+
return builderExecutionStaleBefore(now);
|
|
211
266
|
}
|
|
212
267
|
|
|
213
268
|
export function builderExecutionAffectedRows(result: unknown) {
|
|
269
|
+
if (!result || typeof result !== "object") return 0;
|
|
214
270
|
return (
|
|
215
271
|
(
|
|
216
272
|
result as {
|
|
@@ -233,14 +289,18 @@ function isReclaimableRunningExecution(
|
|
|
233
289
|
execution: BuilderSourceExecutionRecord,
|
|
234
290
|
now: string,
|
|
235
291
|
) {
|
|
292
|
+
if (
|
|
293
|
+
execution.state === "reconciliation_required" &&
|
|
294
|
+
execution.lastError === BUILDER_RESPONSE_CHECKPOINT_FAILURE
|
|
295
|
+
) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
236
298
|
if (
|
|
237
299
|
execution.state !== "running" &&
|
|
238
300
|
execution.state !== "reconciliation_required"
|
|
239
301
|
)
|
|
240
302
|
return true;
|
|
241
|
-
|
|
242
|
-
if (!Number.isFinite(updatedAt)) return false;
|
|
243
|
-
return updatedAt < Date.parse(staleRunningCutoff(now));
|
|
303
|
+
return isBuilderExecutionClaimStale(execution.updatedAt, now);
|
|
244
304
|
}
|
|
245
305
|
|
|
246
306
|
function executionResponsePayload(args: {
|
|
@@ -478,6 +538,14 @@ function livePreflightBlockMessage(args: {
|
|
|
478
538
|
if (!args.liveState.exists) {
|
|
479
539
|
return "Builder entry no longer exists; refresh the source.";
|
|
480
540
|
}
|
|
541
|
+
// Two missing timestamps compare equal, so an absent value must block
|
|
542
|
+
// rather than read as "unchanged".
|
|
543
|
+
if (!args.baselineLastUpdated?.trim()) {
|
|
544
|
+
return "This change has no reviewed Builder update time; refresh and re-review.";
|
|
545
|
+
}
|
|
546
|
+
if (toEpochMs(args.liveState.lastUpdated) === null) {
|
|
547
|
+
return "Builder did not report when this entry last changed; refresh and re-review.";
|
|
548
|
+
}
|
|
481
549
|
if (
|
|
482
550
|
liveTimestampsDiffer({
|
|
483
551
|
liveLastUpdated: args.liveState.lastUpdated,
|
|
@@ -647,6 +715,105 @@ export function realExecutionDeps(
|
|
|
647
715
|
sourceId?: string,
|
|
648
716
|
changeSetId?: string,
|
|
649
717
|
): ExecuteBuilderSourceExecutionDeps {
|
|
718
|
+
const storedPayloadJson = async (executionId: string, payload: unknown) => {
|
|
719
|
+
const [execution] = await getDb()
|
|
720
|
+
.select()
|
|
721
|
+
.from(schema.contentDatabaseSourceExecutions)
|
|
722
|
+
.where(eq(schema.contentDatabaseSourceExecutions.id, executionId))
|
|
723
|
+
.limit(1);
|
|
724
|
+
if (!execution)
|
|
725
|
+
fail("Builder execution disappeared.", {
|
|
726
|
+
errorCode: "builder_execution_changed",
|
|
727
|
+
statusCode: 409,
|
|
728
|
+
});
|
|
729
|
+
const payloadJson = await storeBuilderExecutionPayload({
|
|
730
|
+
payload,
|
|
731
|
+
binding: {
|
|
732
|
+
ownerEmail: execution.ownerEmail,
|
|
733
|
+
sourceId: execution.sourceId,
|
|
734
|
+
changeSetId: execution.changeSetId,
|
|
735
|
+
executionId: execution.id,
|
|
736
|
+
idempotencyKey: execution.idempotencyKey,
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
return {
|
|
740
|
+
payloadJson,
|
|
741
|
+
previousPayloadJson: execution.payloadJson,
|
|
742
|
+
previousState: execution.state,
|
|
743
|
+
previousAttemptToken: execution.attemptToken,
|
|
744
|
+
previousReference: builderExecutionPayloadReference(
|
|
745
|
+
execution.payloadJson,
|
|
746
|
+
),
|
|
747
|
+
nextReference: builderExecutionPayloadReference(payloadJson),
|
|
748
|
+
};
|
|
749
|
+
};
|
|
750
|
+
const previousAttemptFilter = (prepared: {
|
|
751
|
+
previousAttemptToken: string | null;
|
|
752
|
+
}) =>
|
|
753
|
+
prepared.previousAttemptToken
|
|
754
|
+
? eq(
|
|
755
|
+
schema.contentDatabaseSourceExecutions.attemptToken,
|
|
756
|
+
prepared.previousAttemptToken,
|
|
757
|
+
)
|
|
758
|
+
: isNull(schema.contentDatabaseSourceExecutions.attemptToken);
|
|
759
|
+
const discardUncommittedPayload = async (prepared: {
|
|
760
|
+
nextReference: string | null;
|
|
761
|
+
}) => {
|
|
762
|
+
if (prepared.nextReference) {
|
|
763
|
+
await cleanupBuilderPrivatePayload(
|
|
764
|
+
prepared.nextReference,
|
|
765
|
+
"uncommitted execution payload",
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
const updateStoredPayload = async (
|
|
770
|
+
prepared: Awaited<ReturnType<typeof storedPayloadJson>>,
|
|
771
|
+
update: (tx: ReturnType<typeof getDb>) => PromiseLike<unknown>,
|
|
772
|
+
) =>
|
|
773
|
+
getDb().transaction(async (tx) => {
|
|
774
|
+
const transactionDb = tx as unknown as ReturnType<typeof getDb>;
|
|
775
|
+
const result = await update(transactionDb);
|
|
776
|
+
if (
|
|
777
|
+
builderExecutionAffectedRows(result) > 0 &&
|
|
778
|
+
prepared.previousReference &&
|
|
779
|
+
prepared.previousReference !== prepared.nextReference
|
|
780
|
+
) {
|
|
781
|
+
await enqueueBuilderPrivatePayloadCleanup(
|
|
782
|
+
transactionDb,
|
|
783
|
+
prepared.previousReference,
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
return result;
|
|
787
|
+
});
|
|
788
|
+
const resolveAmbiguousPayloadWrite = async (
|
|
789
|
+
executionId: string,
|
|
790
|
+
prepared: {
|
|
791
|
+
payloadJson: string;
|
|
792
|
+
previousReference: string | null;
|
|
793
|
+
nextReference: string | null;
|
|
794
|
+
},
|
|
795
|
+
) => {
|
|
796
|
+
try {
|
|
797
|
+
const [current] = await getDb()
|
|
798
|
+
.select({
|
|
799
|
+
payloadJson: schema.contentDatabaseSourceExecutions.payloadJson,
|
|
800
|
+
})
|
|
801
|
+
.from(schema.contentDatabaseSourceExecutions)
|
|
802
|
+
.where(eq(schema.contentDatabaseSourceExecutions.id, executionId))
|
|
803
|
+
.limit(1);
|
|
804
|
+
if (current?.payloadJson === prepared.payloadJson) {
|
|
805
|
+
return "committed" as const;
|
|
806
|
+
}
|
|
807
|
+
if (current) {
|
|
808
|
+
await discardUncommittedPayload(prepared);
|
|
809
|
+
return "orphan" as const;
|
|
810
|
+
}
|
|
811
|
+
} catch {
|
|
812
|
+
// coercion-ok: an unavailable readback is explicitly the unknown outcome.
|
|
813
|
+
// Inconclusive readback retains both refs for a later SQL-truth reload.
|
|
814
|
+
}
|
|
815
|
+
return "unknown" as const;
|
|
816
|
+
};
|
|
650
817
|
return {
|
|
651
818
|
now: () => new Date().toISOString(),
|
|
652
819
|
resolveDatabase: (args) => resolveDatabaseForSourceMutation(args),
|
|
@@ -724,64 +891,263 @@ export function realExecutionDeps(
|
|
|
724
891
|
) {
|
|
725
892
|
return null;
|
|
726
893
|
}
|
|
727
|
-
|
|
894
|
+
if (!execution) return null;
|
|
895
|
+
const payload = await readBuilderExecutionPayload({
|
|
896
|
+
payloadJson: execution.payloadJson,
|
|
897
|
+
binding: {
|
|
898
|
+
ownerEmail: execution.ownerEmail,
|
|
899
|
+
sourceId: execution.sourceId,
|
|
900
|
+
changeSetId: execution.changeSetId,
|
|
901
|
+
executionId: execution.id,
|
|
902
|
+
idempotencyKey: execution.idempotencyKey,
|
|
903
|
+
},
|
|
904
|
+
});
|
|
905
|
+
return { ...execution, payloadJson: JSON.stringify(payload) };
|
|
728
906
|
},
|
|
729
907
|
updateExecutionState: async (args) => {
|
|
730
|
-
await
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
908
|
+
const prepared = await storedPayloadJson(args.executionId, args.payload);
|
|
909
|
+
let result;
|
|
910
|
+
try {
|
|
911
|
+
result = await updateStoredPayload(prepared, (tx) =>
|
|
912
|
+
tx
|
|
913
|
+
.update(schema.contentDatabaseSourceExecutions)
|
|
914
|
+
.set({
|
|
915
|
+
state: args.state,
|
|
916
|
+
summary: args.summary,
|
|
917
|
+
payloadJson: prepared.payloadJson,
|
|
918
|
+
lastError: args.lastError,
|
|
919
|
+
updatedAt: args.now,
|
|
920
|
+
})
|
|
921
|
+
.where(
|
|
922
|
+
and(
|
|
923
|
+
eq(schema.contentDatabaseSourceExecutions.id, args.executionId),
|
|
924
|
+
eq(
|
|
925
|
+
schema.contentDatabaseSourceExecutions.payloadJson,
|
|
926
|
+
prepared.previousPayloadJson,
|
|
927
|
+
),
|
|
928
|
+
eq(
|
|
929
|
+
schema.contentDatabaseSourceExecutions.state,
|
|
930
|
+
prepared.previousState,
|
|
931
|
+
),
|
|
932
|
+
previousAttemptFilter(prepared),
|
|
933
|
+
eq(
|
|
934
|
+
schema.contentDatabaseSourceExecutions.state,
|
|
935
|
+
args.expected.state,
|
|
936
|
+
),
|
|
937
|
+
eq(
|
|
938
|
+
schema.contentDatabaseSourceExecutions.updatedAt,
|
|
939
|
+
args.expected.updatedAt,
|
|
940
|
+
),
|
|
941
|
+
previousAttemptFilter({
|
|
942
|
+
previousAttemptToken: args.expected.attemptToken ?? null,
|
|
943
|
+
}),
|
|
944
|
+
),
|
|
945
|
+
),
|
|
946
|
+
);
|
|
947
|
+
} catch (error) {
|
|
948
|
+
if (
|
|
949
|
+
(await resolveAmbiguousPayloadWrite(args.executionId, prepared)) ===
|
|
950
|
+
"committed"
|
|
951
|
+
) {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
throw error;
|
|
955
|
+
}
|
|
956
|
+
if (builderExecutionAffectedRows(result) === 0) {
|
|
957
|
+
await discardUncommittedPayload(prepared);
|
|
958
|
+
fail("Builder execution changed before state update.", {
|
|
959
|
+
errorCode: "builder_execution_changed",
|
|
960
|
+
statusCode: 409,
|
|
961
|
+
});
|
|
962
|
+
}
|
|
740
963
|
},
|
|
741
964
|
claimExecution: async (args) => {
|
|
965
|
+
const prepared = await storedPayloadJson(args.executionId, args.payload);
|
|
966
|
+
let result;
|
|
967
|
+
try {
|
|
968
|
+
result = await getDb().transaction(async (tx) => {
|
|
969
|
+
const transactionDb = tx as unknown as ReturnType<typeof getDb>;
|
|
970
|
+
const [scope] = await tx
|
|
971
|
+
.select({ databaseId: schema.contentDatabaseSources.databaseId })
|
|
972
|
+
.from(schema.contentDatabaseSourceExecutions)
|
|
973
|
+
.innerJoin(
|
|
974
|
+
schema.contentDatabaseSources,
|
|
975
|
+
eq(
|
|
976
|
+
schema.contentDatabaseSourceExecutions.sourceId,
|
|
977
|
+
schema.contentDatabaseSources.id,
|
|
978
|
+
),
|
|
979
|
+
)
|
|
980
|
+
.where(
|
|
981
|
+
eq(schema.contentDatabaseSourceExecutions.id, args.executionId),
|
|
982
|
+
)
|
|
983
|
+
.limit(1);
|
|
984
|
+
if (!scope) return null;
|
|
985
|
+
await lockContentDatabaseMutation(transactionDb, scope.databaseId);
|
|
986
|
+
const updated = await tx
|
|
987
|
+
.update(schema.contentDatabaseSourceExecutions)
|
|
988
|
+
.set({
|
|
989
|
+
state: "running",
|
|
990
|
+
summary: args.summary,
|
|
991
|
+
payloadJson: prepared.payloadJson,
|
|
992
|
+
lastError: null,
|
|
993
|
+
attemptToken: args.attemptToken,
|
|
994
|
+
updatedAt: args.now,
|
|
995
|
+
})
|
|
996
|
+
.where(
|
|
997
|
+
and(
|
|
998
|
+
eq(schema.contentDatabaseSourceExecutions.id, args.executionId),
|
|
999
|
+
eq(
|
|
1000
|
+
schema.contentDatabaseSourceExecutions.payloadJson,
|
|
1001
|
+
prepared.previousPayloadJson,
|
|
1002
|
+
),
|
|
1003
|
+
eq(
|
|
1004
|
+
schema.contentDatabaseSourceExecutions.state,
|
|
1005
|
+
prepared.previousState,
|
|
1006
|
+
),
|
|
1007
|
+
previousAttemptFilter(prepared),
|
|
1008
|
+
eq(
|
|
1009
|
+
schema.contentDatabaseSourceExecutions.state,
|
|
1010
|
+
args.expected.state,
|
|
1011
|
+
),
|
|
1012
|
+
eq(
|
|
1013
|
+
schema.contentDatabaseSourceExecutions.updatedAt,
|
|
1014
|
+
args.expected.updatedAt,
|
|
1015
|
+
),
|
|
1016
|
+
previousAttemptFilter({
|
|
1017
|
+
previousAttemptToken: args.expected.attemptToken ?? null,
|
|
1018
|
+
}),
|
|
1019
|
+
exists(
|
|
1020
|
+
tx
|
|
1021
|
+
.select({ id: schema.contentDatabaseSourceChangeSets.id })
|
|
1022
|
+
.from(schema.contentDatabaseSourceChangeSets)
|
|
1023
|
+
.where(
|
|
1024
|
+
and(
|
|
1025
|
+
eq(
|
|
1026
|
+
schema.contentDatabaseSourceChangeSets.id,
|
|
1027
|
+
args.changeSetId,
|
|
1028
|
+
),
|
|
1029
|
+
eq(
|
|
1030
|
+
schema.contentDatabaseSourceChangeSets.state,
|
|
1031
|
+
"approved",
|
|
1032
|
+
),
|
|
1033
|
+
),
|
|
1034
|
+
),
|
|
1035
|
+
),
|
|
1036
|
+
or(
|
|
1037
|
+
notInArray(schema.contentDatabaseSourceExecutions.state, [
|
|
1038
|
+
"running",
|
|
1039
|
+
"succeeded",
|
|
1040
|
+
]),
|
|
1041
|
+
and(
|
|
1042
|
+
eq(schema.contentDatabaseSourceExecutions.state, "running"),
|
|
1043
|
+
lt(
|
|
1044
|
+
schema.contentDatabaseSourceExecutions.updatedAt,
|
|
1045
|
+
args.staleBefore,
|
|
1046
|
+
),
|
|
1047
|
+
),
|
|
1048
|
+
),
|
|
1049
|
+
),
|
|
1050
|
+
);
|
|
1051
|
+
if (
|
|
1052
|
+
builderExecutionAffectedRows(updated) > 0 &&
|
|
1053
|
+
prepared.previousReference &&
|
|
1054
|
+
prepared.previousReference !== prepared.nextReference
|
|
1055
|
+
) {
|
|
1056
|
+
await enqueueBuilderPrivatePayloadCleanup(
|
|
1057
|
+
transactionDb,
|
|
1058
|
+
prepared.previousReference,
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
return updated;
|
|
1062
|
+
});
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
if (
|
|
1065
|
+
(await resolveAmbiguousPayloadWrite(args.executionId, prepared)) ===
|
|
1066
|
+
"committed"
|
|
1067
|
+
) {
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1070
|
+
throw error;
|
|
1071
|
+
}
|
|
1072
|
+
if (builderExecutionAffectedRows(result) === 0) {
|
|
1073
|
+
await discardUncommittedPayload(prepared);
|
|
1074
|
+
}
|
|
1075
|
+
return builderExecutionAffectedRows(result) > 0;
|
|
1076
|
+
},
|
|
1077
|
+
renewExecutionClaim: async (args) => {
|
|
742
1078
|
const result = await getDb()
|
|
743
1079
|
.update(schema.contentDatabaseSourceExecutions)
|
|
744
|
-
.set({
|
|
745
|
-
state: "running",
|
|
746
|
-
summary: args.summary,
|
|
747
|
-
payloadJson: JSON.stringify(args.payload),
|
|
748
|
-
lastError: null,
|
|
749
|
-
attemptToken: args.attemptToken,
|
|
750
|
-
updatedAt: args.now,
|
|
751
|
-
})
|
|
1080
|
+
.set({ updatedAt: args.now })
|
|
752
1081
|
.where(
|
|
753
1082
|
and(
|
|
754
1083
|
eq(schema.contentDatabaseSourceExecutions.id, args.executionId),
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
1084
|
+
eq(schema.contentDatabaseSourceExecutions.state, "running"),
|
|
1085
|
+
eq(
|
|
1086
|
+
schema.contentDatabaseSourceExecutions.attemptToken,
|
|
1087
|
+
args.attemptToken,
|
|
1088
|
+
),
|
|
1089
|
+
),
|
|
1090
|
+
)
|
|
1091
|
+
.returning({ id: schema.contentDatabaseSourceExecutions.id });
|
|
1092
|
+
return result.length > 0;
|
|
1093
|
+
},
|
|
1094
|
+
checkpointResponse: async (args) => {
|
|
1095
|
+
const prepared = await storedPayloadJson(args.executionId, args.payload);
|
|
1096
|
+
let result;
|
|
1097
|
+
try {
|
|
1098
|
+
result = await updateStoredPayload(prepared, (tx) =>
|
|
1099
|
+
tx
|
|
1100
|
+
.update(schema.contentDatabaseSourceExecutions)
|
|
1101
|
+
.set({
|
|
1102
|
+
state: "response_received",
|
|
1103
|
+
summary: "Builder response received; reconciling locally.",
|
|
1104
|
+
payloadJson: prepared.payloadJson,
|
|
1105
|
+
lastError: null,
|
|
1106
|
+
updatedAt: args.now,
|
|
1107
|
+
})
|
|
1108
|
+
.where(
|
|
760
1109
|
and(
|
|
1110
|
+
eq(schema.contentDatabaseSourceExecutions.id, args.executionId),
|
|
1111
|
+
eq(
|
|
1112
|
+
schema.contentDatabaseSourceExecutions.payloadJson,
|
|
1113
|
+
prepared.previousPayloadJson,
|
|
1114
|
+
),
|
|
761
1115
|
eq(schema.contentDatabaseSourceExecutions.state, "running"),
|
|
762
|
-
|
|
763
|
-
schema.contentDatabaseSourceExecutions.
|
|
764
|
-
args.
|
|
1116
|
+
eq(
|
|
1117
|
+
schema.contentDatabaseSourceExecutions.attemptToken,
|
|
1118
|
+
args.attemptToken,
|
|
765
1119
|
),
|
|
766
1120
|
),
|
|
767
1121
|
),
|
|
768
|
-
),
|
|
769
1122
|
);
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
if (
|
|
1125
|
+
(await resolveAmbiguousPayloadWrite(args.executionId, prepared)) ===
|
|
1126
|
+
"committed"
|
|
1127
|
+
) {
|
|
1128
|
+
return true;
|
|
1129
|
+
}
|
|
1130
|
+
throw error;
|
|
1131
|
+
}
|
|
1132
|
+
if (builderExecutionAffectedRows(result) === 0) {
|
|
1133
|
+
await discardUncommittedPayload(prepared);
|
|
1134
|
+
}
|
|
770
1135
|
return builderExecutionAffectedRows(result) > 0;
|
|
771
1136
|
},
|
|
772
|
-
|
|
1137
|
+
markResponseCheckpointFailed: async (args) => {
|
|
773
1138
|
const result = await getDb()
|
|
774
1139
|
.update(schema.contentDatabaseSourceExecutions)
|
|
775
1140
|
.set({
|
|
776
|
-
state: "
|
|
777
|
-
summary:
|
|
778
|
-
|
|
779
|
-
lastError:
|
|
1141
|
+
state: "reconciliation_required",
|
|
1142
|
+
summary:
|
|
1143
|
+
"Builder write response could not be stored for reconciliation.",
|
|
1144
|
+
lastError: args.lastError,
|
|
780
1145
|
updatedAt: args.now,
|
|
781
1146
|
})
|
|
782
1147
|
.where(
|
|
783
1148
|
and(
|
|
784
1149
|
eq(schema.contentDatabaseSourceExecutions.id, args.executionId),
|
|
1150
|
+
eq(schema.contentDatabaseSourceExecutions.state, "running"),
|
|
785
1151
|
eq(
|
|
786
1152
|
schema.contentDatabaseSourceExecutions.attemptToken,
|
|
787
1153
|
args.attemptToken,
|
|
@@ -792,65 +1158,329 @@ export function realExecutionDeps(
|
|
|
792
1158
|
},
|
|
793
1159
|
markExecutionSucceeded: async (args) => {
|
|
794
1160
|
const db = getDb();
|
|
795
|
-
await
|
|
796
|
-
|
|
1161
|
+
const prepared = await storedPayloadJson(args.executionId, args.payload);
|
|
1162
|
+
try {
|
|
1163
|
+
await db.transaction(async (tx) => {
|
|
1164
|
+
const result = await tx
|
|
1165
|
+
.update(schema.contentDatabaseSourceExecutions)
|
|
1166
|
+
.set({
|
|
1167
|
+
state: "succeeded",
|
|
1168
|
+
summary: args.summary,
|
|
1169
|
+
payloadJson: prepared.payloadJson,
|
|
1170
|
+
lastError: null,
|
|
1171
|
+
updatedAt: args.now,
|
|
1172
|
+
})
|
|
1173
|
+
.where(
|
|
1174
|
+
args.attemptToken
|
|
1175
|
+
? and(
|
|
1176
|
+
eq(
|
|
1177
|
+
schema.contentDatabaseSourceExecutions.id,
|
|
1178
|
+
args.executionId,
|
|
1179
|
+
),
|
|
1180
|
+
eq(
|
|
1181
|
+
schema.contentDatabaseSourceExecutions.attemptToken,
|
|
1182
|
+
args.attemptToken,
|
|
1183
|
+
),
|
|
1184
|
+
eq(
|
|
1185
|
+
schema.contentDatabaseSourceExecutions.payloadJson,
|
|
1186
|
+
prepared.previousPayloadJson,
|
|
1187
|
+
),
|
|
1188
|
+
eq(
|
|
1189
|
+
schema.contentDatabaseSourceExecutions.state,
|
|
1190
|
+
prepared.previousState,
|
|
1191
|
+
),
|
|
1192
|
+
)
|
|
1193
|
+
: and(
|
|
1194
|
+
eq(
|
|
1195
|
+
schema.contentDatabaseSourceExecutions.id,
|
|
1196
|
+
args.executionId,
|
|
1197
|
+
),
|
|
1198
|
+
eq(
|
|
1199
|
+
schema.contentDatabaseSourceExecutions.payloadJson,
|
|
1200
|
+
prepared.previousPayloadJson,
|
|
1201
|
+
),
|
|
1202
|
+
eq(
|
|
1203
|
+
schema.contentDatabaseSourceExecutions.state,
|
|
1204
|
+
prepared.previousState,
|
|
1205
|
+
),
|
|
1206
|
+
previousAttemptFilter(prepared),
|
|
1207
|
+
),
|
|
1208
|
+
);
|
|
1209
|
+
if (builderExecutionAffectedRows(result) === 0) {
|
|
1210
|
+
fail("Execution lease was reclaimed before completion.", {
|
|
1211
|
+
errorCode: "builder_execution_changed",
|
|
1212
|
+
statusCode: 409,
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
if (
|
|
1216
|
+
prepared.previousReference &&
|
|
1217
|
+
prepared.previousReference !== prepared.nextReference
|
|
1218
|
+
) {
|
|
1219
|
+
await enqueueBuilderPrivatePayloadCleanup(
|
|
1220
|
+
tx as unknown as ReturnType<typeof getDb>,
|
|
1221
|
+
prepared.previousReference,
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
await tx
|
|
1225
|
+
.update(schema.contentDatabaseSourceChangeSets)
|
|
1226
|
+
.set({ state: "applied", updatedAt: args.now })
|
|
1227
|
+
.where(
|
|
1228
|
+
eq(schema.contentDatabaseSourceChangeSets.id, args.changeSetId),
|
|
1229
|
+
);
|
|
1230
|
+
});
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
if (
|
|
1233
|
+
(await resolveAmbiguousPayloadWrite(args.executionId, prepared)) ===
|
|
1234
|
+
"committed"
|
|
1235
|
+
) {
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
throw error;
|
|
1239
|
+
}
|
|
1240
|
+
},
|
|
1241
|
+
markExecutionFailed: async (args) => {
|
|
1242
|
+
let prepared: Awaited<ReturnType<typeof storedPayloadJson>>;
|
|
1243
|
+
try {
|
|
1244
|
+
prepared = await storedPayloadJson(args.executionId, args.payload);
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
if (args.state !== "reconciliation_required") throw error;
|
|
1247
|
+
const [current] = await getDb()
|
|
1248
|
+
.select()
|
|
1249
|
+
.from(schema.contentDatabaseSourceExecutions)
|
|
1250
|
+
.where(
|
|
1251
|
+
eq(schema.contentDatabaseSourceExecutions.id, args.executionId),
|
|
1252
|
+
)
|
|
1253
|
+
.limit(1);
|
|
1254
|
+
if (!current) throw error;
|
|
1255
|
+
const result = await getDb()
|
|
797
1256
|
.update(schema.contentDatabaseSourceExecutions)
|
|
798
1257
|
.set({
|
|
799
|
-
state: "
|
|
1258
|
+
state: "reconciliation_required",
|
|
800
1259
|
summary: args.summary,
|
|
801
|
-
|
|
802
|
-
|
|
1260
|
+
lastError:
|
|
1261
|
+
args.lastError ??
|
|
1262
|
+
"Builder outcome requires reconciliation; payload could not be checkpointed.",
|
|
803
1263
|
updatedAt: args.now,
|
|
804
1264
|
})
|
|
805
1265
|
.where(
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1266
|
+
and(
|
|
1267
|
+
eq(schema.contentDatabaseSourceExecutions.id, args.executionId),
|
|
1268
|
+
eq(
|
|
1269
|
+
schema.contentDatabaseSourceExecutions.payloadJson,
|
|
1270
|
+
current.payloadJson,
|
|
1271
|
+
),
|
|
1272
|
+
eq(schema.contentDatabaseSourceExecutions.state, current.state),
|
|
1273
|
+
notInArray(schema.contentDatabaseSourceExecutions.state, [
|
|
1274
|
+
"succeeded",
|
|
1275
|
+
"failed",
|
|
1276
|
+
]),
|
|
1277
|
+
args.attemptToken
|
|
1278
|
+
? eq(
|
|
813
1279
|
schema.contentDatabaseSourceExecutions.attemptToken,
|
|
814
1280
|
args.attemptToken,
|
|
815
|
-
)
|
|
816
|
-
)
|
|
817
|
-
|
|
1281
|
+
)
|
|
1282
|
+
: isNull(schema.contentDatabaseSourceExecutions.attemptToken),
|
|
1283
|
+
),
|
|
1284
|
+
)
|
|
1285
|
+
.returning({ id: schema.contentDatabaseSourceExecutions.id });
|
|
1286
|
+
if (result.length === 0) {
|
|
1287
|
+
fail(
|
|
1288
|
+
"Builder execution changed before reconciliation was recorded.",
|
|
1289
|
+
{
|
|
1290
|
+
errorCode: "builder_execution_changed",
|
|
1291
|
+
statusCode: 409,
|
|
1292
|
+
},
|
|
818
1293
|
);
|
|
819
|
-
if (args.attemptToken && builderExecutionAffectedRows(result) === 0) {
|
|
820
|
-
throw new Error("Execution lease was reclaimed before completion.");
|
|
821
1294
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
let result;
|
|
1298
|
+
try {
|
|
1299
|
+
result = await updateStoredPayload(prepared, (tx) =>
|
|
1300
|
+
tx
|
|
1301
|
+
.update(schema.contentDatabaseSourceExecutions)
|
|
1302
|
+
.set({
|
|
1303
|
+
state: args.state ?? "failed",
|
|
1304
|
+
summary: args.summary,
|
|
1305
|
+
payloadJson: prepared.payloadJson,
|
|
1306
|
+
lastError: args.lastError,
|
|
1307
|
+
updatedAt: args.now,
|
|
1308
|
+
})
|
|
1309
|
+
.where(
|
|
1310
|
+
args.attemptToken
|
|
1311
|
+
? and(
|
|
1312
|
+
eq(
|
|
1313
|
+
schema.contentDatabaseSourceExecutions.id,
|
|
1314
|
+
args.executionId,
|
|
1315
|
+
),
|
|
1316
|
+
eq(
|
|
1317
|
+
schema.contentDatabaseSourceExecutions.attemptToken,
|
|
1318
|
+
args.attemptToken,
|
|
1319
|
+
),
|
|
1320
|
+
eq(
|
|
1321
|
+
schema.contentDatabaseSourceExecutions.payloadJson,
|
|
1322
|
+
prepared.previousPayloadJson,
|
|
1323
|
+
),
|
|
1324
|
+
eq(
|
|
1325
|
+
schema.contentDatabaseSourceExecutions.state,
|
|
1326
|
+
prepared.previousState,
|
|
1327
|
+
),
|
|
1328
|
+
)
|
|
1329
|
+
: and(
|
|
1330
|
+
eq(
|
|
1331
|
+
schema.contentDatabaseSourceExecutions.id,
|
|
1332
|
+
args.executionId,
|
|
1333
|
+
),
|
|
1334
|
+
eq(
|
|
1335
|
+
schema.contentDatabaseSourceExecutions.payloadJson,
|
|
1336
|
+
prepared.previousPayloadJson,
|
|
1337
|
+
),
|
|
1338
|
+
eq(
|
|
1339
|
+
schema.contentDatabaseSourceExecutions.state,
|
|
1340
|
+
prepared.previousState,
|
|
1341
|
+
),
|
|
1342
|
+
previousAttemptFilter(prepared),
|
|
1343
|
+
),
|
|
1344
|
+
),
|
|
1345
|
+
);
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
if (
|
|
1348
|
+
(await resolveAmbiguousPayloadWrite(args.executionId, prepared)) ===
|
|
1349
|
+
"committed"
|
|
1350
|
+
) {
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
throw error;
|
|
1354
|
+
}
|
|
1355
|
+
if (builderExecutionAffectedRows(result) === 0) {
|
|
1356
|
+
await discardUncommittedPayload(prepared);
|
|
1357
|
+
fail("Builder execution changed before failure checkpoint.", {
|
|
1358
|
+
errorCode: "builder_execution_changed",
|
|
1359
|
+
statusCode: 409,
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
},
|
|
1363
|
+
releaseExecutionClaim: async (args) => {
|
|
1364
|
+
let prepared: Awaited<ReturnType<typeof storedPayloadJson>>;
|
|
1365
|
+
try {
|
|
1366
|
+
prepared = await storedPayloadJson(args.executionId, args.payload);
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
if (
|
|
1369
|
+
!isActionContractError(error) ||
|
|
1370
|
+
error.errorCode !== "BUILDER_PRIVATE_PAYLOAD_UNAVAILABLE"
|
|
1371
|
+
) {
|
|
1372
|
+
throw error;
|
|
1373
|
+
}
|
|
1374
|
+
// Nothing was sent, so the stored payload is still accurate. Blob
|
|
1375
|
+
// storage being down must not hold the claim until its lease expires.
|
|
1376
|
+
// Cancelling a blocked gate needs the preflight evidence that could
|
|
1377
|
+
// not be stored; a ready gate stays cancellable and re-checks the
|
|
1378
|
+
// live entry on its next run.
|
|
1379
|
+
const result = await getDb()
|
|
1380
|
+
.update(schema.contentDatabaseSourceExecutions)
|
|
1381
|
+
.set({
|
|
1382
|
+
state:
|
|
1383
|
+
args.restore.state === "blocked" ? "ready" : args.restore.state,
|
|
1384
|
+
summary: args.summary,
|
|
1385
|
+
lastError: args.lastError,
|
|
1386
|
+
attemptToken: args.restore.attemptToken ?? null,
|
|
1387
|
+
updatedAt:
|
|
1388
|
+
args.restore.state === "running"
|
|
1389
|
+
? args.restore.updatedAt
|
|
1390
|
+
: args.now,
|
|
1391
|
+
})
|
|
825
1392
|
.where(
|
|
826
|
-
|
|
1393
|
+
and(
|
|
1394
|
+
eq(schema.contentDatabaseSourceExecutions.id, args.executionId),
|
|
1395
|
+
eq(schema.contentDatabaseSourceExecutions.state, "running"),
|
|
1396
|
+
eq(
|
|
1397
|
+
schema.contentDatabaseSourceExecutions.attemptToken,
|
|
1398
|
+
args.attemptToken,
|
|
1399
|
+
),
|
|
1400
|
+
),
|
|
827
1401
|
);
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1402
|
+
if (builderExecutionAffectedRows(result) === 0) {
|
|
1403
|
+
fail("Builder execution changed before its claim was released.", {
|
|
1404
|
+
errorCode: "builder_execution_changed",
|
|
1405
|
+
statusCode: 409,
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
let result;
|
|
1411
|
+
try {
|
|
1412
|
+
result = await updateStoredPayload(prepared, (tx) =>
|
|
1413
|
+
tx
|
|
1414
|
+
.update(schema.contentDatabaseSourceExecutions)
|
|
1415
|
+
.set({
|
|
1416
|
+
state: args.restore.state,
|
|
1417
|
+
summary: args.summary,
|
|
1418
|
+
payloadJson: prepared.payloadJson,
|
|
1419
|
+
lastError: args.lastError,
|
|
1420
|
+
attemptToken: args.restore.attemptToken ?? null,
|
|
1421
|
+
updatedAt:
|
|
1422
|
+
args.restore.state === "running"
|
|
1423
|
+
? args.restore.updatedAt
|
|
1424
|
+
: args.now,
|
|
1425
|
+
})
|
|
1426
|
+
.where(
|
|
1427
|
+
and(
|
|
843
1428
|
eq(schema.contentDatabaseSourceExecutions.id, args.executionId),
|
|
1429
|
+
eq(schema.contentDatabaseSourceExecutions.state, "running"),
|
|
844
1430
|
eq(
|
|
845
1431
|
schema.contentDatabaseSourceExecutions.attemptToken,
|
|
846
1432
|
args.attemptToken,
|
|
847
1433
|
),
|
|
848
|
-
|
|
849
|
-
|
|
1434
|
+
eq(
|
|
1435
|
+
schema.contentDatabaseSourceExecutions.payloadJson,
|
|
1436
|
+
prepared.previousPayloadJson,
|
|
1437
|
+
),
|
|
1438
|
+
),
|
|
1439
|
+
),
|
|
850
1440
|
);
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
if (
|
|
1443
|
+
(await resolveAmbiguousPayloadWrite(args.executionId, prepared)) ===
|
|
1444
|
+
"committed"
|
|
1445
|
+
) {
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
throw error;
|
|
1449
|
+
}
|
|
1450
|
+
if (builderExecutionAffectedRows(result) === 0) {
|
|
1451
|
+
await discardUncommittedPayload(prepared);
|
|
1452
|
+
fail("Builder execution changed before its claim was released.", {
|
|
1453
|
+
errorCode: "builder_execution_changed",
|
|
1454
|
+
statusCode: 409,
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
851
1457
|
},
|
|
852
1458
|
executeWrite: (args) => executeBuilderCmsWrite(args),
|
|
853
|
-
readLiveEntry: (args) =>
|
|
1459
|
+
readLiveEntry: async (args) => {
|
|
1460
|
+
const controller = new AbortController();
|
|
1461
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
1462
|
+
const deadline = new Promise<never>((_resolve, reject) => {
|
|
1463
|
+
timeout = setTimeout(() => {
|
|
1464
|
+
const error = new Error(
|
|
1465
|
+
"Builder live preflight exceeded its time limit.",
|
|
1466
|
+
);
|
|
1467
|
+
controller.abort(error);
|
|
1468
|
+
reject(error);
|
|
1469
|
+
}, BUILDER_LIVE_PREFLIGHT_TIMEOUT_MS);
|
|
1470
|
+
});
|
|
1471
|
+
try {
|
|
1472
|
+
return await Promise.race([
|
|
1473
|
+
readBuilderCmsEntryLiveState({
|
|
1474
|
+
...args,
|
|
1475
|
+
requireBoundOAuth: true,
|
|
1476
|
+
signal: controller.signal,
|
|
1477
|
+
}),
|
|
1478
|
+
deadline,
|
|
1479
|
+
]);
|
|
1480
|
+
} finally {
|
|
1481
|
+
if (timeout) clearTimeout(timeout);
|
|
1482
|
+
}
|
|
1483
|
+
},
|
|
854
1484
|
reconcileWrite: reconcileBuilderCmsWrite,
|
|
855
1485
|
getResponse: (databaseId) =>
|
|
856
1486
|
getContentDatabaseResponse(databaseId, { limit: 100, offset: 0 }),
|
|
@@ -949,9 +1579,12 @@ export async function executeBuilderSourceExecutionWithDeps(
|
|
|
949
1579
|
!isReclaimableRunningExecution(execution, now)
|
|
950
1580
|
) {
|
|
951
1581
|
throw builderExecutionConflict(
|
|
952
|
-
execution.state === "reconciliation_required"
|
|
953
|
-
|
|
954
|
-
|
|
1582
|
+
execution.state === "reconciliation_required" &&
|
|
1583
|
+
execution.lastError === BUILDER_RESPONSE_CHECKPOINT_FAILURE
|
|
1584
|
+
? BUILDER_RESPONSE_CHECKPOINT_FAILURE
|
|
1585
|
+
: execution.state === "reconciliation_required"
|
|
1586
|
+
? "Builder reconciliation required; wait for the recovery window before a read-only lookup. Do not retry."
|
|
1587
|
+
: "Builder execution is already running.",
|
|
955
1588
|
);
|
|
956
1589
|
}
|
|
957
1590
|
|
|
@@ -996,6 +1629,11 @@ export async function executeBuilderSourceExecutionWithDeps(
|
|
|
996
1629
|
: (plan.lastError ?? "Builder execution is blocked.");
|
|
997
1630
|
await deps.updateExecutionState({
|
|
998
1631
|
executionId: execution.id,
|
|
1632
|
+
expected: {
|
|
1633
|
+
state: execution.state,
|
|
1634
|
+
updatedAt: execution.updatedAt,
|
|
1635
|
+
attemptToken: execution.attemptToken,
|
|
1636
|
+
},
|
|
999
1637
|
state: "blocked",
|
|
1000
1638
|
summary: `${plan.summary} Execution blocked before write.`,
|
|
1001
1639
|
payload: validatedPayload,
|
|
@@ -1008,6 +1646,11 @@ export async function executeBuilderSourceExecutionWithDeps(
|
|
|
1008
1646
|
const message = plan.lastError ?? "Builder execution is not ready.";
|
|
1009
1647
|
await deps.updateExecutionState({
|
|
1010
1648
|
executionId: execution.id,
|
|
1649
|
+
expected: {
|
|
1650
|
+
state: execution.state,
|
|
1651
|
+
updatedAt: execution.updatedAt,
|
|
1652
|
+
attemptToken: execution.attemptToken,
|
|
1653
|
+
},
|
|
1011
1654
|
state: plan.state,
|
|
1012
1655
|
summary: plan.summary,
|
|
1013
1656
|
payload: validatedPayload,
|
|
@@ -1021,6 +1664,11 @@ export async function executeBuilderSourceExecutionWithDeps(
|
|
|
1021
1664
|
const message = "Live Builder writes are disabled for this source.";
|
|
1022
1665
|
await deps.updateExecutionState({
|
|
1023
1666
|
executionId: execution.id,
|
|
1667
|
+
expected: {
|
|
1668
|
+
state: execution.state,
|
|
1669
|
+
updatedAt: execution.updatedAt,
|
|
1670
|
+
attemptToken: execution.attemptToken,
|
|
1671
|
+
},
|
|
1024
1672
|
state: "write_disabled",
|
|
1025
1673
|
summary: `${plan.summary} Execution blocked before write.`,
|
|
1026
1674
|
payload: validatedPayload,
|
|
@@ -1037,10 +1685,14 @@ export async function executeBuilderSourceExecutionWithDeps(
|
|
|
1037
1685
|
? {
|
|
1038
1686
|
exactTitle: storedIntent?.exactTitle,
|
|
1039
1687
|
intendedFields: storedIntent?.intendedFields,
|
|
1688
|
+
expectedSourceSpace: source.metadata.builderSpacePublicKey,
|
|
1689
|
+
expectedSourceConnectionId: source.metadata.connectionId,
|
|
1040
1690
|
}
|
|
1041
1691
|
: {
|
|
1042
1692
|
marker: planIntent.marker,
|
|
1043
1693
|
intendedFields: planIntent.intendedFields,
|
|
1694
|
+
expectedSourceSpace: source.metadata.builderSpacePublicKey,
|
|
1695
|
+
expectedSourceConnectionId: source.metadata.connectionId,
|
|
1044
1696
|
},
|
|
1045
1697
|
);
|
|
1046
1698
|
const matchingIntentCount = lookup.matchingIntentCount ?? lookup.count;
|
|
@@ -1123,31 +1775,117 @@ export async function executeBuilderSourceExecutionWithDeps(
|
|
|
1123
1775
|
return result;
|
|
1124
1776
|
}
|
|
1125
1777
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1778
|
+
const preflightEntryId = requiresLivePreflight(plan.payload.effect)
|
|
1779
|
+
? plan.payload.target.entryId
|
|
1780
|
+
: null;
|
|
1781
|
+
if (requiresLivePreflight(plan.payload.effect) && !preflightEntryId) {
|
|
1782
|
+
const message = "Builder entry no longer exists; refresh the source.";
|
|
1783
|
+
await deps.updateExecutionState({
|
|
1784
|
+
executionId: execution.id,
|
|
1785
|
+
expected: {
|
|
1786
|
+
state: execution.state,
|
|
1787
|
+
updatedAt: execution.updatedAt,
|
|
1788
|
+
attemptToken: execution.attemptToken,
|
|
1789
|
+
},
|
|
1790
|
+
state: "blocked",
|
|
1791
|
+
summary: `${plan.summary} Execution blocked before write.`,
|
|
1792
|
+
payload: validatedPayload,
|
|
1793
|
+
lastError: message,
|
|
1794
|
+
now,
|
|
1795
|
+
});
|
|
1796
|
+
throw new Error(message);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
const attemptToken = crypto.randomUUID();
|
|
1800
|
+
const claimed = await deps.claimExecution({
|
|
1801
|
+
executionId: execution.id,
|
|
1802
|
+
changeSetId: changeSet.id,
|
|
1803
|
+
expected: {
|
|
1804
|
+
state: execution.state,
|
|
1805
|
+
updatedAt: execution.updatedAt,
|
|
1806
|
+
attemptToken: execution.attemptToken,
|
|
1807
|
+
},
|
|
1808
|
+
summary: `Running Builder ${plan.pushMode} execution.`,
|
|
1809
|
+
payload: validatedPayload,
|
|
1810
|
+
now,
|
|
1811
|
+
staleBefore: staleRunningCutoff(now),
|
|
1812
|
+
attemptToken,
|
|
1813
|
+
});
|
|
1814
|
+
if (!claimed) {
|
|
1815
|
+
throw builderExecutionConflict("Builder execution is already running.");
|
|
1816
|
+
}
|
|
1817
|
+
// Nothing has reached Builder until executeWrite returns a result, so any
|
|
1818
|
+
// stop before then hands the gate back instead of stranding a claim that
|
|
1819
|
+
// cancel and review must treat as an attempted write.
|
|
1820
|
+
const releaseClaim = (args: {
|
|
1821
|
+
state: BuilderSourceExecutionRecord["state"];
|
|
1822
|
+
summary: string;
|
|
1823
|
+
payload: unknown;
|
|
1824
|
+
lastError: string;
|
|
1825
|
+
}) =>
|
|
1826
|
+
deps.releaseExecutionClaim({
|
|
1827
|
+
executionId: execution.id,
|
|
1828
|
+
attemptToken,
|
|
1829
|
+
restore: {
|
|
1830
|
+
state: args.state,
|
|
1831
|
+
attemptToken: execution.attemptToken,
|
|
1832
|
+
updatedAt: execution.updatedAt,
|
|
1833
|
+
},
|
|
1834
|
+
summary: args.summary,
|
|
1835
|
+
payload: args.payload,
|
|
1836
|
+
lastError: args.lastError,
|
|
1837
|
+
now: deps.now(),
|
|
1838
|
+
});
|
|
1839
|
+
|
|
1840
|
+
if (preflightEntryId) {
|
|
1841
|
+
// This check runs after the claim so it sits as close to the write as
|
|
1842
|
+
// possible. A Builder save that lands after this read and before the
|
|
1843
|
+
// write below is still overwritten: the check narrows that race but
|
|
1844
|
+
// cannot close it without a conditional write on Builder's side.
|
|
1845
|
+
let liveState: BuilderCmsEntryLiveState;
|
|
1846
|
+
try {
|
|
1847
|
+
liveState = await deps.readLiveEntry({
|
|
1848
|
+
model: plan.payload.target.model,
|
|
1849
|
+
entryId: preflightEntryId,
|
|
1850
|
+
expectedSourceSpace: source.metadata.builderSpacePublicKey,
|
|
1851
|
+
expectedSourceConnectionId: source.metadata.connectionId,
|
|
1852
|
+
});
|
|
1853
|
+
} catch (error) {
|
|
1854
|
+
await releaseClaim({
|
|
1855
|
+
state: execution.state,
|
|
1856
|
+
summary: `${plan.summary} Execution stopped before write.`,
|
|
1857
|
+
payload: validatedPayload,
|
|
1858
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
1859
|
+
});
|
|
1860
|
+
throw error;
|
|
1861
|
+
}
|
|
1862
|
+
let claimRenewed: boolean;
|
|
1863
|
+
try {
|
|
1864
|
+
claimRenewed = await deps.renewExecutionClaim({
|
|
1131
1865
|
executionId: execution.id,
|
|
1132
|
-
|
|
1133
|
-
|
|
1866
|
+
attemptToken,
|
|
1867
|
+
now: deps.now(),
|
|
1868
|
+
});
|
|
1869
|
+
} catch (error) {
|
|
1870
|
+
await releaseClaim({
|
|
1871
|
+
state: execution.state,
|
|
1872
|
+
summary: `${plan.summary} Execution stopped before write.`,
|
|
1134
1873
|
payload: validatedPayload,
|
|
1135
|
-
lastError: message,
|
|
1136
|
-
now,
|
|
1874
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
1137
1875
|
});
|
|
1138
|
-
throw
|
|
1876
|
+
throw error;
|
|
1139
1877
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
}
|
|
1145
|
-
const
|
|
1878
|
+
if (!claimRenewed) {
|
|
1879
|
+
throw builderExecutionConflict(
|
|
1880
|
+
"Builder execution lease was reclaimed during live preflight.",
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
const baselineLastUpdated = plan.payload.target.reviewedLastUpdated;
|
|
1146
1884
|
console.info("builder_source_live_preflight", {
|
|
1147
1885
|
executionId: execution.id,
|
|
1148
1886
|
changeSetId: changeSet.id,
|
|
1149
|
-
targetEntryId:
|
|
1150
|
-
baselineLastUpdated
|
|
1887
|
+
targetEntryId: preflightEntryId,
|
|
1888
|
+
baselineLastUpdated,
|
|
1151
1889
|
liveLastUpdated: liveState.lastUpdated,
|
|
1152
1890
|
baselineBlocksHash: changeSet.bodyChange?.currentHash ?? null,
|
|
1153
1891
|
liveBlocksHash: liveState.blocksHash,
|
|
@@ -1155,13 +1893,12 @@ export async function executeBuilderSourceExecutionWithDeps(
|
|
|
1155
1893
|
});
|
|
1156
1894
|
const message = livePreflightBlockMessage({
|
|
1157
1895
|
liveState,
|
|
1158
|
-
baselineLastUpdated
|
|
1896
|
+
baselineLastUpdated,
|
|
1159
1897
|
baselineBlocksHash: changeSet.bodyChange?.currentHash ?? null,
|
|
1160
1898
|
effect: plan.payload.effect,
|
|
1161
1899
|
});
|
|
1162
1900
|
if (message) {
|
|
1163
|
-
await
|
|
1164
|
-
executionId: execution.id,
|
|
1901
|
+
await releaseClaim({
|
|
1165
1902
|
state: "blocked",
|
|
1166
1903
|
summary: `${plan.summary} Execution blocked before write.`,
|
|
1167
1904
|
payload: {
|
|
@@ -1176,29 +1913,35 @@ export async function executeBuilderSourceExecutionWithDeps(
|
|
|
1176
1913
|
},
|
|
1177
1914
|
},
|
|
1178
1915
|
lastError: message,
|
|
1179
|
-
now,
|
|
1180
1916
|
});
|
|
1181
|
-
|
|
1917
|
+
fail(message, {
|
|
1918
|
+
errorCode: "builder_entry_changed_since_review",
|
|
1919
|
+
statusCode: 409,
|
|
1920
|
+
});
|
|
1182
1921
|
}
|
|
1183
1922
|
}
|
|
1184
|
-
|
|
1185
|
-
const attemptToken = crypto.randomUUID();
|
|
1186
|
-
const claimed = await deps.claimExecution({
|
|
1187
|
-
executionId: execution.id,
|
|
1188
|
-
summary: `Running Builder ${plan.pushMode} execution.`,
|
|
1189
|
-
payload: validatedPayload,
|
|
1190
|
-
now,
|
|
1191
|
-
staleBefore: staleRunningCutoff(now),
|
|
1192
|
-
attemptToken,
|
|
1193
|
-
});
|
|
1194
|
-
if (!claimed) {
|
|
1195
|
-
throw builderExecutionConflict("Builder execution is already running.");
|
|
1196
|
-
}
|
|
1197
1923
|
timing.record("approval_gate_and_dry_run_validation", gateStartedAt);
|
|
1198
1924
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1925
|
+
let writeResult: BuilderCmsWriteResult;
|
|
1926
|
+
try {
|
|
1927
|
+
writeResult = await timing.measure("write_dispatch", () =>
|
|
1928
|
+
deps.executeWrite({
|
|
1929
|
+
request: plan.payload.request,
|
|
1930
|
+
expectedSourceSpace: source.metadata.builderSpacePublicKey,
|
|
1931
|
+
expectedSourceConnectionId: source.metadata.connectionId,
|
|
1932
|
+
requireSourceBinding: true,
|
|
1933
|
+
}),
|
|
1934
|
+
);
|
|
1935
|
+
} catch (error) {
|
|
1936
|
+
// executeWrite reports every post-dispatch outcome as a result.
|
|
1937
|
+
await releaseClaim({
|
|
1938
|
+
state: execution.state,
|
|
1939
|
+
summary: `${plan.summary} Execution stopped before write.`,
|
|
1940
|
+
payload: validatedPayload,
|
|
1941
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
1942
|
+
});
|
|
1943
|
+
throw error;
|
|
1944
|
+
}
|
|
1202
1945
|
const payloadWithResponse = executionResponsePayload({
|
|
1203
1946
|
payload: validatedPayload,
|
|
1204
1947
|
writeResult,
|
|
@@ -1230,27 +1973,44 @@ export async function executeBuilderSourceExecutionWithDeps(
|
|
|
1230
1973
|
}
|
|
1231
1974
|
|
|
1232
1975
|
const reconciliationStartedAt = timing.start();
|
|
1233
|
-
|
|
1234
|
-
|
|
1976
|
+
let checkpointed = true;
|
|
1977
|
+
try {
|
|
1978
|
+
if (deps.checkpointResponse) {
|
|
1979
|
+
checkpointed = await deps.checkpointResponse({
|
|
1980
|
+
executionId: execution.id,
|
|
1981
|
+
attemptToken,
|
|
1982
|
+
payload: payloadWithResponse,
|
|
1983
|
+
now: deps.now(),
|
|
1984
|
+
});
|
|
1985
|
+
} else {
|
|
1986
|
+
await deps.updateExecutionState({
|
|
1987
|
+
executionId: execution.id,
|
|
1988
|
+
expected: { state: "running", updatedAt: now, attemptToken },
|
|
1989
|
+
state: "response_received",
|
|
1990
|
+
summary: "Builder response received; reconciling locally.",
|
|
1991
|
+
payload: payloadWithResponse,
|
|
1992
|
+
lastError: null,
|
|
1993
|
+
now: deps.now(),
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
} catch {
|
|
1997
|
+
const recorded = await deps.markResponseCheckpointFailed({
|
|
1235
1998
|
executionId: execution.id,
|
|
1236
1999
|
attemptToken,
|
|
1237
|
-
|
|
2000
|
+
lastError: BUILDER_RESPONSE_CHECKPOINT_FAILURE,
|
|
1238
2001
|
now: deps.now(),
|
|
1239
2002
|
});
|
|
1240
|
-
if (!
|
|
1241
|
-
throw
|
|
1242
|
-
"Execution lease was reclaimed before response
|
|
2003
|
+
if (!recorded) {
|
|
2004
|
+
throw builderExecutionConflict(
|
|
2005
|
+
"Execution lease was reclaimed before response custody could be recorded.",
|
|
1243
2006
|
);
|
|
1244
2007
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
lastError: null,
|
|
1252
|
-
now: deps.now(),
|
|
1253
|
-
});
|
|
2008
|
+
throw builderExecutionConflict(BUILDER_RESPONSE_CHECKPOINT_FAILURE);
|
|
2009
|
+
}
|
|
2010
|
+
if (!checkpointed) {
|
|
2011
|
+
throw builderExecutionConflict(
|
|
2012
|
+
"Execution lease was reclaimed before response checkpoint.",
|
|
2013
|
+
);
|
|
1254
2014
|
}
|
|
1255
2015
|
|
|
1256
2016
|
const succeededAt = deps.now();
|