@agent-native/core 0.101.7 → 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 +22 -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/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/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.md +3 -2
- 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 +1041 -154
- package/corpus/templates/content/actions/_database-utils.ts +89 -12
- package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -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/get-preview-document-draft.ts +26 -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/set-content-database-source-write-mode.ts +2 -2
- package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +27 -15
- 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 +2353 -576
- 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 +25 -0
- package/corpus/templates/content/app/i18n-data.ts +287 -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/parity/matrix.md +27 -25
- package/corpus/templates/content/parity/matrix.ts +50 -2
- package/corpus/templates/content/server/db/schema.ts +47 -0
- package/corpus/templates/content/server/plugins/auth.ts +1 -0
- package/corpus/templates/content/server/plugins/db.ts +53 -0
- package/corpus/templates/content/shared/api.ts +70 -1
- package/corpus/templates/content/shared/builder-mdx.ts +705 -15
- package/corpus/templates/content/shared/nfm.ts +3 -3
- 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/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/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- 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
|
@@ -36,6 +36,32 @@ type DatabaseMembershipRow = {
|
|
|
36
36
|
bodyHydrationQueueId?: string | null;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
type DocumentListRow = Omit<typeof schema.documents.$inferSelect, "content">;
|
|
40
|
+
|
|
41
|
+
// Database grids render row metadata and properties. Fetching the document body
|
|
42
|
+
// here would transfer it only for serializeDocument to replace it with an empty
|
|
43
|
+
// string below; opened documents use their dedicated document read path instead.
|
|
44
|
+
export const contentDatabaseListDocumentSelection = {
|
|
45
|
+
id: schema.documents.id,
|
|
46
|
+
parentId: schema.documents.parentId,
|
|
47
|
+
title: schema.documents.title,
|
|
48
|
+
description: schema.documents.description,
|
|
49
|
+
icon: schema.documents.icon,
|
|
50
|
+
position: schema.documents.position,
|
|
51
|
+
isFavorite: schema.documents.isFavorite,
|
|
52
|
+
hideFromSearch: schema.documents.hideFromSearch,
|
|
53
|
+
sourceMode: schema.documents.sourceMode,
|
|
54
|
+
sourceKind: schema.documents.sourceKind,
|
|
55
|
+
sourcePath: schema.documents.sourcePath,
|
|
56
|
+
sourceRootPath: schema.documents.sourceRootPath,
|
|
57
|
+
sourceUpdatedAt: schema.documents.sourceUpdatedAt,
|
|
58
|
+
visibility: schema.documents.visibility,
|
|
59
|
+
ownerEmail: schema.documents.ownerEmail,
|
|
60
|
+
orgId: schema.documents.orgId,
|
|
61
|
+
createdAt: schema.documents.createdAt,
|
|
62
|
+
updatedAt: schema.documents.updatedAt,
|
|
63
|
+
};
|
|
64
|
+
|
|
39
65
|
export function serializeBodyHydration(
|
|
40
66
|
item: typeof schema.contentDatabaseItems.$inferSelect,
|
|
41
67
|
options: { queued?: boolean } = {},
|
|
@@ -46,6 +72,7 @@ export function serializeBodyHydration(
|
|
|
46
72
|
status === "pending" ||
|
|
47
73
|
status === "hydrating" ||
|
|
48
74
|
status === "hydrated" ||
|
|
75
|
+
status === "unavailable" ||
|
|
49
76
|
status === "error"
|
|
50
77
|
? status
|
|
51
78
|
: options.queued
|
|
@@ -123,16 +150,61 @@ export function normalizeContentDatabasePageOptions(options: {
|
|
|
123
150
|
return { limit, offset };
|
|
124
151
|
}
|
|
125
152
|
|
|
153
|
+
export function filterContentDatabaseSourceRowsForPage<
|
|
154
|
+
TRow extends { documentId: string; databaseItemId: string },
|
|
155
|
+
TChangeSet extends {
|
|
156
|
+
documentId: string | null;
|
|
157
|
+
databaseItemId: string | null;
|
|
158
|
+
direction: string;
|
|
159
|
+
state: string;
|
|
160
|
+
executions: Array<{ state: string }>;
|
|
161
|
+
},
|
|
162
|
+
>(args: {
|
|
163
|
+
rows: TRow[];
|
|
164
|
+
changeSets: TChangeSet[];
|
|
165
|
+
visibleDocumentIds: ReadonlySet<string>;
|
|
166
|
+
}) {
|
|
167
|
+
const actionableChangeSets = args.changeSets.filter(
|
|
168
|
+
(changeSet) =>
|
|
169
|
+
changeSet.direction === "outbound" &&
|
|
170
|
+
!changeSet.executions.some(
|
|
171
|
+
(execution) => execution.state === "succeeded",
|
|
172
|
+
) &&
|
|
173
|
+
(changeSet.state === "pending_push" ||
|
|
174
|
+
changeSet.state === "staged_revision" ||
|
|
175
|
+
changeSet.state === "approved"),
|
|
176
|
+
);
|
|
177
|
+
const actionableDocumentIds = new Set(
|
|
178
|
+
actionableChangeSets.flatMap((changeSet) =>
|
|
179
|
+
changeSet.documentId ? [changeSet.documentId] : [],
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
const actionableItemIds = new Set(
|
|
183
|
+
actionableChangeSets.flatMap((changeSet) =>
|
|
184
|
+
changeSet.databaseItemId ? [changeSet.databaseItemId] : [],
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
return args.rows.filter(
|
|
189
|
+
(row) =>
|
|
190
|
+
!row.documentId ||
|
|
191
|
+
args.visibleDocumentIds.has(row.documentId) ||
|
|
192
|
+
actionableDocumentIds.has(row.documentId) ||
|
|
193
|
+
actionableItemIds.has(row.databaseItemId),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
126
197
|
function serializeDocument(
|
|
127
|
-
doc:
|
|
198
|
+
doc: DocumentListRow,
|
|
128
199
|
membership?: DatabaseMembershipRow,
|
|
129
|
-
options: { includeContent?: boolean } = {},
|
|
130
200
|
) {
|
|
131
201
|
return {
|
|
132
202
|
id: doc.id,
|
|
133
203
|
parentId: doc.parentId,
|
|
134
204
|
title: doc.title,
|
|
135
|
-
|
|
205
|
+
// List reads deliberately project no `documents.content`; opened documents
|
|
206
|
+
// use their dedicated read path.
|
|
207
|
+
content: "",
|
|
136
208
|
description: doc.description,
|
|
137
209
|
icon: doc.icon,
|
|
138
210
|
position: doc.position,
|
|
@@ -196,7 +268,7 @@ export async function getContentDatabaseResponse(
|
|
|
196
268
|
const documents =
|
|
197
269
|
items.length > 0
|
|
198
270
|
? await db
|
|
199
|
-
.select()
|
|
271
|
+
.select(contentDatabaseListDocumentSelection)
|
|
200
272
|
.from(schema.documents)
|
|
201
273
|
.where(
|
|
202
274
|
and(
|
|
@@ -211,7 +283,9 @@ export async function getContentDatabaseResponse(
|
|
|
211
283
|
const documentById = new Map(documents.map((doc) => [doc.id, doc]));
|
|
212
284
|
const propertiesByDocumentId = await listPropertiesForDatabaseDocuments(
|
|
213
285
|
databaseId,
|
|
214
|
-
|
|
286
|
+
// Property serialization uses metadata only; this list projection carries
|
|
287
|
+
// every document field it consumes except the deliberately omitted body.
|
|
288
|
+
documents as Array<typeof schema.documents.$inferSelect>,
|
|
215
289
|
);
|
|
216
290
|
const queuedBodyHydrationItemIds =
|
|
217
291
|
items.length > 0
|
|
@@ -259,17 +333,20 @@ export async function getContentDatabaseResponse(
|
|
|
259
333
|
serializedItems.map((item) => item.document.id),
|
|
260
334
|
);
|
|
261
335
|
// When paginating, scope every DOCUMENT-BACKED source's rows to the visible
|
|
262
|
-
// page
|
|
263
|
-
//
|
|
264
|
-
//
|
|
336
|
+
// page, plus the small set referenced by actionable reviews. The dialog gets
|
|
337
|
+
// change sets independently of the item page and needs those rows to retain
|
|
338
|
+
// the linked provider target instead of misclassifying an off-page update as
|
|
339
|
+
// a create. Federated join rows carry no document (empty documentId), so
|
|
340
|
+
// they're kept intact — only matched ones overlay anyway.
|
|
265
341
|
const pagedSources =
|
|
266
342
|
limit !== null
|
|
267
343
|
? sources.map((source) => ({
|
|
268
344
|
...source,
|
|
269
|
-
rows:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
345
|
+
rows: filterContentDatabaseSourceRowsForPage({
|
|
346
|
+
rows: source.rows,
|
|
347
|
+
changeSets: source.changeSets,
|
|
348
|
+
visibleDocumentIds: serializedDocumentIds,
|
|
349
|
+
}),
|
|
273
350
|
}))
|
|
274
351
|
: sources;
|
|
275
352
|
const pagedPrimary = pagedSources[0] ?? null;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { and, eq } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
4
|
+
|
|
5
|
+
export async function readPreviewDocumentDraft(
|
|
6
|
+
ownerEmail: string,
|
|
7
|
+
orgId: string,
|
|
8
|
+
documentId: string,
|
|
9
|
+
) {
|
|
10
|
+
const [draft] = await getDb()
|
|
11
|
+
.select({
|
|
12
|
+
documentId: schema.documentPreviewDrafts.documentId,
|
|
13
|
+
title: schema.documentPreviewDrafts.title,
|
|
14
|
+
content: schema.documentPreviewDrafts.content,
|
|
15
|
+
baseDocumentUpdatedAt: schema.documentPreviewDrafts.baseDocumentUpdatedAt,
|
|
16
|
+
loadedContentWasEmpty: schema.documentPreviewDrafts.loadedContentWasEmpty,
|
|
17
|
+
deferredReason: schema.documentPreviewDrafts.deferredReason,
|
|
18
|
+
version: schema.documentPreviewDrafts.version,
|
|
19
|
+
updatedAt: schema.documentPreviewDrafts.updatedAt,
|
|
20
|
+
})
|
|
21
|
+
.from(schema.documentPreviewDrafts)
|
|
22
|
+
.where(
|
|
23
|
+
and(
|
|
24
|
+
eq(schema.documentPreviewDrafts.ownerEmail, ownerEmail),
|
|
25
|
+
eq(schema.documentPreviewDrafts.orgId, orgId),
|
|
26
|
+
eq(schema.documentPreviewDrafts.documentId, documentId),
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
.limit(1);
|
|
30
|
+
return draft ?? null;
|
|
31
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
3
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
4
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
8
|
+
import type {
|
|
9
|
+
CancelPreparedBuilderSourceUpdateRequest,
|
|
10
|
+
CancelPreparedBuilderSourceUpdateResponse,
|
|
11
|
+
} from "../shared/api.js";
|
|
12
|
+
import {
|
|
13
|
+
CANCELLED_BUILDER_EXECUTION_SUMMARY,
|
|
14
|
+
CANCELLED_BUILDER_REVIEW_NOTE_PREFIX,
|
|
15
|
+
resolveDatabaseForSourceMutation,
|
|
16
|
+
} from "./_database-source-utils.js";
|
|
17
|
+
import { getContentDatabaseResponse } from "./_database-utils.js";
|
|
18
|
+
|
|
19
|
+
const PRE_DISPATCH_STATES = new Set(["ready", "write_disabled", "blocked"]);
|
|
20
|
+
|
|
21
|
+
function parseExecutionPayload(payloadJson: string) {
|
|
22
|
+
let payload: unknown;
|
|
23
|
+
try {
|
|
24
|
+
payload = JSON.parse(payloadJson);
|
|
25
|
+
} catch {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"Cannot cancel this Builder update because its execution evidence is unreadable.",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
"Cannot cancel this Builder update because its execution evidence is ambiguous.",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return payload as Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isLocalBlockedDryRun(payload: Record<string, unknown>) {
|
|
39
|
+
const dryRun = payload.dryRun;
|
|
40
|
+
return (
|
|
41
|
+
!!dryRun &&
|
|
42
|
+
typeof dryRun === "object" &&
|
|
43
|
+
!Array.isArray(dryRun) &&
|
|
44
|
+
(dryRun as Record<string, unknown>).status === "blocked"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function assertProvablyPreDispatch(execution: {
|
|
49
|
+
state: string;
|
|
50
|
+
summary: string;
|
|
51
|
+
payloadJson: string;
|
|
52
|
+
attemptToken: string | null;
|
|
53
|
+
}) {
|
|
54
|
+
if (!PRE_DISPATCH_STATES.has(execution.state)) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Cannot cancel a Builder execution in state ${execution.state}. Its outcome must be preserved.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (execution.attemptToken) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"Cannot cancel this Builder update because a dispatch attempt was recorded.",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const payload = parseExecutionPayload(execution.payloadJson);
|
|
66
|
+
if (
|
|
67
|
+
Object.prototype.hasOwnProperty.call(payload, "response") ||
|
|
68
|
+
Object.prototype.hasOwnProperty.call(payload, "dispatch")
|
|
69
|
+
) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"Cannot cancel this Builder update because dispatch or response evidence was recorded.",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
execution.state === "blocked" &&
|
|
76
|
+
execution.summary !== CANCELLED_BUILDER_EXECUTION_SUMMARY &&
|
|
77
|
+
!isLocalBlockedDryRun(payload)
|
|
78
|
+
) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
"Cannot cancel this blocked Builder update because it is not provably a local pre-dispatch block.",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function assertCanonicalClaims(
|
|
86
|
+
executions: Array<{ id: string; idempotencyKey: string }>,
|
|
87
|
+
claims: Array<{ idempotencyKey: string; executionId: string }>,
|
|
88
|
+
) {
|
|
89
|
+
const idempotencyKeys = executions.map((row) => row.idempotencyKey);
|
|
90
|
+
if (new Set(idempotencyKeys).size !== executions.length) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
"Cannot cancel this Builder update because duplicate execution gates share an idempotency key.",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (claims.length !== executions.length) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
"Cannot cancel this Builder update because its canonical execution claim is missing or foreign.",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const claimByKey = new Map(
|
|
101
|
+
claims.map((claim) => [claim.idempotencyKey, claim.executionId]),
|
|
102
|
+
);
|
|
103
|
+
for (const execution of executions) {
|
|
104
|
+
if (claimByKey.get(execution.idempotencyKey) !== execution.id) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"Cannot cancel this Builder update because an execution claim points to a different gate.",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default defineAction({
|
|
113
|
+
description:
|
|
114
|
+
"Cancel one exact prepared Builder update only when every execution is provably pre-dispatch. This never calls Builder or deletes audit history.",
|
|
115
|
+
schema: z
|
|
116
|
+
.object({
|
|
117
|
+
databaseId: z.string().optional().describe("Database ID"),
|
|
118
|
+
documentId: z.string().optional().describe("Database document/page ID"),
|
|
119
|
+
sourceId: z.string().describe("Exact Builder source ID"),
|
|
120
|
+
changeSetId: z.string().describe("Exact prepared Builder change-set ID"),
|
|
121
|
+
note: z
|
|
122
|
+
.string()
|
|
123
|
+
.max(500)
|
|
124
|
+
.optional()
|
|
125
|
+
.describe("Optional cancellation note"),
|
|
126
|
+
})
|
|
127
|
+
.refine((value) => value.databaseId || value.documentId, {
|
|
128
|
+
message: "Provide databaseId or documentId.",
|
|
129
|
+
}),
|
|
130
|
+
run: async (
|
|
131
|
+
args: CancelPreparedBuilderSourceUpdateRequest,
|
|
132
|
+
): Promise<CancelPreparedBuilderSourceUpdateResponse> => {
|
|
133
|
+
const database = await resolveDatabaseForSourceMutation(args);
|
|
134
|
+
if (!database) throw new Error("Database not found.");
|
|
135
|
+
await assertAccess("document", database.documentId, "editor");
|
|
136
|
+
|
|
137
|
+
const db = getDb();
|
|
138
|
+
const actor = getRequestUserEmail() ?? "agent-runtime@agent-native.local";
|
|
139
|
+
const now = new Date().toISOString();
|
|
140
|
+
let executionIds: string[] = [];
|
|
141
|
+
let status: "cancelled" | "already_cancelled" = "cancelled";
|
|
142
|
+
|
|
143
|
+
await db.transaction(async (tx) => {
|
|
144
|
+
const [source] = await tx
|
|
145
|
+
.select()
|
|
146
|
+
.from(schema.contentDatabaseSources)
|
|
147
|
+
.where(
|
|
148
|
+
and(
|
|
149
|
+
eq(schema.contentDatabaseSources.id, args.sourceId),
|
|
150
|
+
eq(schema.contentDatabaseSources.databaseId, database.id),
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
.limit(1);
|
|
154
|
+
if (!source || source.sourceType !== "builder-cms") {
|
|
155
|
+
throw new Error("The exact Builder source was not found.");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const [changeSet] = await tx
|
|
159
|
+
.select()
|
|
160
|
+
.from(schema.contentDatabaseSourceChangeSets)
|
|
161
|
+
.where(
|
|
162
|
+
and(
|
|
163
|
+
eq(schema.contentDatabaseSourceChangeSets.id, args.changeSetId),
|
|
164
|
+
eq(schema.contentDatabaseSourceChangeSets.sourceId, source.id),
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
.limit(1);
|
|
168
|
+
if (!changeSet) throw new Error("Source change-set not found.");
|
|
169
|
+
|
|
170
|
+
const executions = await tx
|
|
171
|
+
.select()
|
|
172
|
+
.from(schema.contentDatabaseSourceExecutions)
|
|
173
|
+
.where(
|
|
174
|
+
and(
|
|
175
|
+
eq(schema.contentDatabaseSourceExecutions.sourceId, source.id),
|
|
176
|
+
eq(
|
|
177
|
+
schema.contentDatabaseSourceExecutions.changeSetId,
|
|
178
|
+
changeSet.id,
|
|
179
|
+
),
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
executionIds = executions.map((execution) => execution.id);
|
|
183
|
+
|
|
184
|
+
if (executions.length === 0) {
|
|
185
|
+
throw new Error("No prepared Builder execution was found to cancel.");
|
|
186
|
+
}
|
|
187
|
+
for (const execution of executions) assertProvablyPreDispatch(execution);
|
|
188
|
+
|
|
189
|
+
const idempotencyKeys = [
|
|
190
|
+
...new Set(executions.map((row) => row.idempotencyKey)),
|
|
191
|
+
];
|
|
192
|
+
const claims = await tx
|
|
193
|
+
.select({
|
|
194
|
+
idempotencyKey:
|
|
195
|
+
schema.contentDatabaseSourceExecutionClaims.idempotencyKey,
|
|
196
|
+
executionId: schema.contentDatabaseSourceExecutionClaims.executionId,
|
|
197
|
+
})
|
|
198
|
+
.from(schema.contentDatabaseSourceExecutionClaims)
|
|
199
|
+
.where(
|
|
200
|
+
and(
|
|
201
|
+
eq(
|
|
202
|
+
schema.contentDatabaseSourceExecutionClaims.ownerEmail,
|
|
203
|
+
database.ownerEmail,
|
|
204
|
+
),
|
|
205
|
+
eq(schema.contentDatabaseSourceExecutionClaims.sourceId, source.id),
|
|
206
|
+
inArray(
|
|
207
|
+
schema.contentDatabaseSourceExecutionClaims.idempotencyKey,
|
|
208
|
+
idempotencyKeys,
|
|
209
|
+
),
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
assertCanonicalClaims(executions, claims);
|
|
213
|
+
|
|
214
|
+
if (changeSet.state === "rejected") {
|
|
215
|
+
const cancellationReviews = await tx
|
|
216
|
+
.select()
|
|
217
|
+
.from(schema.contentDatabaseSourceChangeReviews)
|
|
218
|
+
.where(
|
|
219
|
+
and(
|
|
220
|
+
eq(schema.contentDatabaseSourceChangeReviews.sourceId, source.id),
|
|
221
|
+
eq(
|
|
222
|
+
schema.contentDatabaseSourceChangeReviews.changeSetId,
|
|
223
|
+
changeSet.id,
|
|
224
|
+
),
|
|
225
|
+
eq(
|
|
226
|
+
schema.contentDatabaseSourceChangeReviews.decision,
|
|
227
|
+
"rejected",
|
|
228
|
+
),
|
|
229
|
+
),
|
|
230
|
+
);
|
|
231
|
+
const wasCancelled =
|
|
232
|
+
executions.length > 0 &&
|
|
233
|
+
executions.every(
|
|
234
|
+
(execution) =>
|
|
235
|
+
execution.state === "blocked" &&
|
|
236
|
+
execution.summary === CANCELLED_BUILDER_EXECUTION_SUMMARY,
|
|
237
|
+
) &&
|
|
238
|
+
cancellationReviews.some((review) =>
|
|
239
|
+
review.note?.startsWith(CANCELLED_BUILDER_REVIEW_NOTE_PREFIX),
|
|
240
|
+
);
|
|
241
|
+
if (!wasCancelled) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
"This Builder change-set was rejected through another review path, not cancelled as a prepared update.",
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
status = "already_cancelled";
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (changeSet.state !== "approved") {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`Only an approved, prepared Builder change-set can be cancelled; current state is ${changeSet.state}.`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await tx
|
|
257
|
+
.update(schema.contentDatabaseSourceExecutions)
|
|
258
|
+
.set({
|
|
259
|
+
state: "blocked",
|
|
260
|
+
summary: CANCELLED_BUILDER_EXECUTION_SUMMARY,
|
|
261
|
+
lastError: null,
|
|
262
|
+
updatedAt: now,
|
|
263
|
+
})
|
|
264
|
+
.where(
|
|
265
|
+
and(
|
|
266
|
+
eq(schema.contentDatabaseSourceExecutions.sourceId, source.id),
|
|
267
|
+
eq(
|
|
268
|
+
schema.contentDatabaseSourceExecutions.changeSetId,
|
|
269
|
+
changeSet.id,
|
|
270
|
+
),
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const noteSuffix = args.note?.trim() ? ` Note: ${args.note.trim()}` : "";
|
|
275
|
+
await tx.insert(schema.contentDatabaseSourceChangeReviews).values({
|
|
276
|
+
id: crypto.randomUUID(),
|
|
277
|
+
ownerEmail: database.ownerEmail,
|
|
278
|
+
sourceId: source.id,
|
|
279
|
+
changeSetId: changeSet.id,
|
|
280
|
+
reviewerEmail: actor,
|
|
281
|
+
decision: "rejected",
|
|
282
|
+
stateFrom: changeSet.state,
|
|
283
|
+
stateTo: "rejected",
|
|
284
|
+
note: `${CANCELLED_BUILDER_REVIEW_NOTE_PREFIX} by ${actor} at ${now}.${noteSuffix}`,
|
|
285
|
+
createdAt: now,
|
|
286
|
+
});
|
|
287
|
+
await tx
|
|
288
|
+
.update(schema.contentDatabaseSourceChangeSets)
|
|
289
|
+
.set({ state: "rejected", updatedAt: now })
|
|
290
|
+
.where(eq(schema.contentDatabaseSourceChangeSets.id, changeSet.id));
|
|
291
|
+
await tx
|
|
292
|
+
.update(schema.contentDatabaseSources)
|
|
293
|
+
.set({ updatedAt: now })
|
|
294
|
+
.where(eq(schema.contentDatabaseSources.id, source.id));
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
...(await getContentDatabaseResponse(database.id)),
|
|
299
|
+
cancellation: {
|
|
300
|
+
sourceId: args.sourceId,
|
|
301
|
+
changeSetId: args.changeSetId,
|
|
302
|
+
executionIds,
|
|
303
|
+
status,
|
|
304
|
+
cancelledAt: now,
|
|
305
|
+
cancelledBy: actor,
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
},
|
|
309
|
+
});
|