@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
|
@@ -5,6 +5,7 @@ import { defineAction, type ActionRunContext } from "@agent-native/core/action";
|
|
|
5
5
|
import { writeAppState } from "@agent-native/core/application-state";
|
|
6
6
|
import { withCommittedActionAudit } from "@agent-native/core/audit";
|
|
7
7
|
import { agentTouchDocument } from "@agent-native/core/collab";
|
|
8
|
+
import { getDbExec } from "@agent-native/core/db";
|
|
8
9
|
import {
|
|
9
10
|
iconValueSchema,
|
|
10
11
|
parseIconValue,
|
|
@@ -49,12 +50,20 @@ import {
|
|
|
49
50
|
parseDocumentFavorite,
|
|
50
51
|
parseDocumentHideFromSearch,
|
|
51
52
|
} from "../server/lib/documents.js";
|
|
53
|
+
import {
|
|
54
|
+
liveBodyWriterGateForRow,
|
|
55
|
+
liveBodyWriterPageColumns,
|
|
56
|
+
} from "../server/lib/live-body-writer.js";
|
|
52
57
|
import {
|
|
53
58
|
syncPrivateCalloutReferences,
|
|
54
59
|
syncPrivateIconReference,
|
|
55
60
|
verifyPrivateIconAssignment,
|
|
56
61
|
} from "../server/lib/private-icon-references.js";
|
|
57
|
-
import
|
|
62
|
+
import { liveCopyHoldsSave } from "../server/live-copy/holds.js";
|
|
63
|
+
import type {
|
|
64
|
+
DocumentUpdateResponse,
|
|
65
|
+
LiveCopyBodySave,
|
|
66
|
+
} from "../shared/api.js";
|
|
58
67
|
import { applyContentPersonalNavigationPatch } from "../shared/content-personal-navigation-patch.js";
|
|
59
68
|
import { mergeDocumentBodyIntents } from "../shared/document-intent-merge.js";
|
|
60
69
|
import { inspectNfmFidelity } from "../shared/nfm.js";
|
|
@@ -206,6 +215,13 @@ function documentUpdateResponse(
|
|
|
206
215
|
source: serializeDocumentSource(doc),
|
|
207
216
|
softDeletedDatabaseIds,
|
|
208
217
|
...(browserSaveAttempt ? { browserSaveAttempt } : {}),
|
|
218
|
+
// Only a save the live copy holds keeps a receipt.
|
|
219
|
+
...(browserSaveAttempt?.bodySave
|
|
220
|
+
? {
|
|
221
|
+
bodySave: browserSaveAttempt.bodySave,
|
|
222
|
+
bodyIntentOutcome: { status: "applied" as const },
|
|
223
|
+
}
|
|
224
|
+
: {}),
|
|
209
225
|
};
|
|
210
226
|
}
|
|
211
227
|
|
|
@@ -974,7 +990,7 @@ const updateDocumentAction = defineAction({
|
|
|
974
990
|
args.authoredCandidateContent === undefined
|
|
975
991
|
? undefined
|
|
976
992
|
: normalizeAuthoredContent(args.authoredCandidateContent);
|
|
977
|
-
let
|
|
993
|
+
let staleBodySave: "stale_builder_image" | "stale_empty_body" | undefined;
|
|
978
994
|
if (content !== undefined && !args.preserveLeadingTitleHeading) {
|
|
979
995
|
if (
|
|
980
996
|
content.includes('componentName="Image"') &&
|
|
@@ -1012,12 +1028,11 @@ const updateDocumentAction = defineAction({
|
|
|
1012
1028
|
sourceContent,
|
|
1013
1029
|
})
|
|
1014
1030
|
) {
|
|
1015
|
-
|
|
1016
|
-
measurement.reason_code = "stale_builder_image";
|
|
1017
|
-
content = existing.content;
|
|
1031
|
+
staleBodySave = "stale_builder_image";
|
|
1018
1032
|
}
|
|
1019
1033
|
}
|
|
1020
1034
|
if (
|
|
1035
|
+
!staleBodySave &&
|
|
1021
1036
|
shouldRejectStaleEmptyBodySave({
|
|
1022
1037
|
incomingContent: content,
|
|
1023
1038
|
currentContent: existing.content,
|
|
@@ -1026,13 +1041,26 @@ const updateDocumentAction = defineAction({
|
|
|
1026
1041
|
loadedContentWasEmpty: args.loadedContentWasEmpty,
|
|
1027
1042
|
})
|
|
1028
1043
|
) {
|
|
1029
|
-
|
|
1030
|
-
measurement.stale_base = "true";
|
|
1031
|
-
measurement.reason_code = "stale_empty_body";
|
|
1032
|
-
content = existing.content;
|
|
1044
|
+
staleBodySave = "stale_empty_body";
|
|
1033
1045
|
}
|
|
1034
1046
|
}
|
|
1035
|
-
|
|
1047
|
+
// These guard a body write, and a switched page's body may not be
|
|
1048
|
+
// written here at all: the gate, read under the page lock, decides.
|
|
1049
|
+
const staleBodySaveDeferred =
|
|
1050
|
+
ctx?.caller === "frontend" && !!existing.liveBodySwitchedAt;
|
|
1051
|
+
/** Keeps the stored body; true when the save must answer a conflict. */
|
|
1052
|
+
const refuseStaleBodySave = (storedContent: string) => {
|
|
1053
|
+
measurement.reason_code = staleBodySave;
|
|
1054
|
+
if (staleBodySave === "stale_empty_body")
|
|
1055
|
+
measurement.stale_base = "true";
|
|
1056
|
+
content = storedContent;
|
|
1057
|
+
return !!args.browserSaveAttemptId;
|
|
1058
|
+
};
|
|
1059
|
+
if (
|
|
1060
|
+
staleBodySave &&
|
|
1061
|
+
!staleBodySaveDeferred &&
|
|
1062
|
+
refuseStaleBodySave(existing.content)
|
|
1063
|
+
) {
|
|
1036
1064
|
measurement.outcome = "conflict";
|
|
1037
1065
|
return respond(
|
|
1038
1066
|
documentConflictResponse(existing, access.role, currentFavorite),
|
|
@@ -1072,6 +1100,11 @@ const updateDocumentAction = defineAction({
|
|
|
1072
1100
|
let browserSaveConfirmation: BrowserSaveAttemptConfirmation | undefined;
|
|
1073
1101
|
let bodyIntentOutcome: BrowserDocumentUpdateResponse["bodyIntentOutcome"];
|
|
1074
1102
|
let discardedEditorGeneration: number | undefined;
|
|
1103
|
+
let liveCopyBodySave: LiveCopyBodySave | undefined;
|
|
1104
|
+
// Only a save the live copy already holds settles its draft or keeps a
|
|
1105
|
+
// receipt. A tab whose updates never reached the live copy has nothing
|
|
1106
|
+
// else on the server holding its text.
|
|
1107
|
+
let settlesLiveCopyBody = false;
|
|
1075
1108
|
let preservationRequired:
|
|
1076
1109
|
| {
|
|
1077
1110
|
reason: "structure" | "provenance";
|
|
@@ -1226,10 +1259,76 @@ const updateDocumentAction = defineAction({
|
|
|
1226
1259
|
contentCasConflict = true;
|
|
1227
1260
|
return;
|
|
1228
1261
|
}
|
|
1262
|
+
if (ctx?.caller === "frontend" && content !== undefined) {
|
|
1263
|
+
const [page] = await tx
|
|
1264
|
+
.select(liveBodyWriterPageColumns)
|
|
1265
|
+
.from(schema.documents)
|
|
1266
|
+
.where(eq(schema.documents.id, id))
|
|
1267
|
+
.limit(1);
|
|
1268
|
+
const gate = await liveBodyWriterGateForRow(page, {
|
|
1269
|
+
transaction: getDbExec(),
|
|
1270
|
+
});
|
|
1271
|
+
if (gate.writePath === "live_copy") {
|
|
1272
|
+
// The editor's updates reach a live copy over collab, so the
|
|
1273
|
+
// body part of its save is not written here. Comparing it with
|
|
1274
|
+
// the stored body would read every peer's live-copy commit as
|
|
1275
|
+
// an unmerged edit and divert this save to History.
|
|
1276
|
+
const saveBodies = new Set(
|
|
1277
|
+
[
|
|
1278
|
+
args.content,
|
|
1279
|
+
args.authoredCandidateContent,
|
|
1280
|
+
args.editorSnapshotContent,
|
|
1281
|
+
].filter((body): body is string => body !== undefined),
|
|
1282
|
+
);
|
|
1283
|
+
const matchesLiveCopy = [...saveBodies].every(
|
|
1284
|
+
(body) => body === historyBefore.content,
|
|
1285
|
+
);
|
|
1286
|
+
liveCopyBodySave = {
|
|
1287
|
+
status: "live_copy",
|
|
1288
|
+
matchesLiveCopy,
|
|
1289
|
+
bodyRevision: historyBefore.bodyRevision,
|
|
1290
|
+
collabVersion: page.liveCopyVersion as number,
|
|
1291
|
+
};
|
|
1292
|
+
// A tab's body trails peers' commits it hasn't received, so
|
|
1293
|
+
// most saves from two open tabs match no stored body. Old
|
|
1294
|
+
// editors read an unsettled save as unsaved and send its text
|
|
1295
|
+
// to History when the tab reopens.
|
|
1296
|
+
// A receipt acknowledges the submitted body, authored candidate
|
|
1297
|
+
// and recovery snapshot. SQL strips title headings they retain.
|
|
1298
|
+
settlesLiveCopyBody = true;
|
|
1299
|
+
for (const body of saveBodies) {
|
|
1300
|
+
if (body === historyBefore.content) continue;
|
|
1301
|
+
const hold = authoredBase
|
|
1302
|
+
? await liveCopyHoldsSave({
|
|
1303
|
+
base: args.authoredBaseContent as string,
|
|
1304
|
+
stored: historyBefore.content,
|
|
1305
|
+
candidate: body,
|
|
1306
|
+
})
|
|
1307
|
+
: undefined;
|
|
1308
|
+
if (hold) measurement.reason_code = `live_copy_save_${hold}`;
|
|
1309
|
+
if (hold !== "held") {
|
|
1310
|
+
settlesLiveCopyBody = false;
|
|
1311
|
+
break;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
if (settlesLiveCopyBody) {
|
|
1315
|
+
bodyIntentOutcome = { status: "applied" };
|
|
1316
|
+
}
|
|
1317
|
+
content = undefined;
|
|
1318
|
+
} else if (
|
|
1319
|
+
staleBodySave &&
|
|
1320
|
+
staleBodySaveDeferred &&
|
|
1321
|
+
refuseStaleBodySave(historyBefore.content)
|
|
1322
|
+
) {
|
|
1323
|
+
contentCasConflict = true;
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1229
1327
|
const confirmBrowserSave = async (
|
|
1230
1328
|
snapshot: { title: string; content: string },
|
|
1231
1329
|
now: string,
|
|
1232
1330
|
) => {
|
|
1331
|
+
if (liveCopyBodySave && !settlesLiveCopyBody) return;
|
|
1233
1332
|
if (
|
|
1234
1333
|
settlesPreviewDraft &&
|
|
1235
1334
|
args.editorSnapshotTitle !== undefined &&
|
|
@@ -1261,17 +1360,23 @@ const updateDocumentAction = defineAction({
|
|
|
1261
1360
|
updatedAt: schema.documents.updatedAt,
|
|
1262
1361
|
})
|
|
1263
1362
|
.from(schema.documents)
|
|
1264
|
-
.where(
|
|
1363
|
+
.where(
|
|
1364
|
+
and(
|
|
1365
|
+
eq(schema.documents.id, id),
|
|
1366
|
+
eq(schema.documents.ownerEmail, ownerEmail),
|
|
1367
|
+
),
|
|
1368
|
+
)
|
|
1265
1369
|
.limit(1);
|
|
1266
1370
|
const confirmation: BrowserSaveAttemptConfirmation = {
|
|
1267
1371
|
attemptId: args.browserSaveAttemptId,
|
|
1268
|
-
result: "applied",
|
|
1372
|
+
result: liveCopyBodySave ? "live_copy" : "applied",
|
|
1269
1373
|
revision: documentRevisionToken(
|
|
1270
1374
|
confirmed.bodyRevision,
|
|
1271
1375
|
confirmed.content,
|
|
1272
1376
|
),
|
|
1273
1377
|
updatedAt: confirmed.updatedAt,
|
|
1274
1378
|
softDeletedDatabaseIds,
|
|
1379
|
+
...(liveCopyBodySave ? { bodySave: liveCopyBodySave } : {}),
|
|
1275
1380
|
};
|
|
1276
1381
|
await tx.insert(schema.documentBrowserSaveAttempts).values({
|
|
1277
1382
|
id: randomUUID(),
|
|
@@ -1333,7 +1438,11 @@ const updateDocumentAction = defineAction({
|
|
|
1333
1438
|
});
|
|
1334
1439
|
}
|
|
1335
1440
|
};
|
|
1336
|
-
if (
|
|
1441
|
+
if (
|
|
1442
|
+
authoredBase &&
|
|
1443
|
+
authoredCandidateContent !== undefined &&
|
|
1444
|
+
!liveCopyBodySave
|
|
1445
|
+
) {
|
|
1337
1446
|
const prior = await findDocumentBodyIntent({
|
|
1338
1447
|
db: tx as ReturnType<typeof getDb>,
|
|
1339
1448
|
ownerEmail,
|
|
@@ -1382,7 +1491,11 @@ const updateDocumentAction = defineAction({
|
|
|
1382
1491
|
{ status: "resolved" }
|
|
1383
1492
|
>
|
|
1384
1493
|
| undefined;
|
|
1385
|
-
if (
|
|
1494
|
+
if (
|
|
1495
|
+
authoredBase &&
|
|
1496
|
+
authoredCandidateContent !== undefined &&
|
|
1497
|
+
!liveCopyBodySave
|
|
1498
|
+
) {
|
|
1386
1499
|
const priorIntents = await readDocumentBodyIntents({
|
|
1387
1500
|
db: tx as ReturnType<typeof getDb>,
|
|
1388
1501
|
ownerEmail,
|
|
@@ -1497,7 +1610,10 @@ const updateDocumentAction = defineAction({
|
|
|
1497
1610
|
if (
|
|
1498
1611
|
intentMerge ||
|
|
1499
1612
|
(useBodyRevisionCas && lockedContentChanged) ||
|
|
1500
|
-
|
|
1613
|
+
// Peers' live-copy commits move updatedAt, so on a live-copy
|
|
1614
|
+
// save the snapshot guards only the fields it still writes.
|
|
1615
|
+
(useDocumentCas &&
|
|
1616
|
+
(!liveCopyBodySave || lockedDocumentFieldsChanged))
|
|
1501
1617
|
) {
|
|
1502
1618
|
const rows = await tx
|
|
1503
1619
|
.update(schema.documents)
|
|
@@ -1596,11 +1712,13 @@ const updateDocumentAction = defineAction({
|
|
|
1596
1712
|
);
|
|
1597
1713
|
}
|
|
1598
1714
|
committedContentChanged = lockedContentChanged;
|
|
1599
|
-
measurement.outcome =
|
|
1600
|
-
?
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1715
|
+
measurement.outcome = liveCopyBodySave
|
|
1716
|
+
? "live_copy_body"
|
|
1717
|
+
: lockedDocumentFieldsChanged
|
|
1718
|
+
? intentMerge && bodyBaseStale && lockedContentChanged
|
|
1719
|
+
? "merged"
|
|
1720
|
+
: "written"
|
|
1721
|
+
: "unchanged";
|
|
1604
1722
|
committedContentBefore = historyBefore.content;
|
|
1605
1723
|
|
|
1606
1724
|
if (lockedTitleChanged && args.title !== undefined) {
|
|
@@ -1924,6 +2042,7 @@ const updateDocumentAction = defineAction({
|
|
|
1924
2042
|
browserSaveConfirmation,
|
|
1925
2043
|
),
|
|
1926
2044
|
...(bodyIntentOutcome ? { bodyIntentOutcome } : {}),
|
|
2045
|
+
...(liveCopyBodySave ? { bodySave: liveCopyBodySave } : {}),
|
|
1927
2046
|
...(creativeContext
|
|
1928
2047
|
? {
|
|
1929
2048
|
contextMode: creativeContext.contextMode,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { defineAction } from "@agent-native/core/action";
|
|
1
|
+
import { defineAction, fail } from "@agent-native/core/action";
|
|
2
2
|
import { assertAccess } from "@agent-native/core/sharing";
|
|
3
|
-
import { and, eq } from "drizzle-orm";
|
|
3
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
@@ -8,6 +8,14 @@ import type {
|
|
|
8
8
|
ContentDatabaseResponse,
|
|
9
9
|
ValidateBuilderSourceExecutionRequest,
|
|
10
10
|
} from "../shared/api.js";
|
|
11
|
+
import {
|
|
12
|
+
builderExecutionPayloadReference,
|
|
13
|
+
cleanupBuilderPrivatePayload,
|
|
14
|
+
enqueueBuilderPrivatePayloadCleanup,
|
|
15
|
+
readBuilderExecutionPayload,
|
|
16
|
+
storeBuilderExecutionPayload,
|
|
17
|
+
} from "./_builder-cms-blob-custody.js";
|
|
18
|
+
import { isBuilderExecutionClaimStale } from "./_builder-cms-execution-lease.js";
|
|
11
19
|
import {
|
|
12
20
|
buildBuilderCmsExecutionPlan,
|
|
13
21
|
builderCmsExecutionIdempotencyKey,
|
|
@@ -19,17 +27,6 @@ import {
|
|
|
19
27
|
} from "./_database-source-utils.js";
|
|
20
28
|
import { getContentDatabaseResponse } from "./_database-utils.js";
|
|
21
29
|
|
|
22
|
-
function parsePayload(value: string) {
|
|
23
|
-
try {
|
|
24
|
-
const parsed = JSON.parse(value) as unknown;
|
|
25
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
26
|
-
? (parsed as Record<string, unknown>)
|
|
27
|
-
: {};
|
|
28
|
-
} catch {
|
|
29
|
-
return {};
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
30
|
export default defineAction({
|
|
34
31
|
description:
|
|
35
32
|
"Validate a prepared Builder CMS execution gate as a local dry run. This rebuilds and compares the write plan, but never calls Builder APIs.",
|
|
@@ -117,10 +114,45 @@ export default defineAction({
|
|
|
117
114
|
"Prepare the Builder execution gate before validating it.",
|
|
118
115
|
);
|
|
119
116
|
}
|
|
120
|
-
|
|
121
117
|
const now = new Date().toISOString();
|
|
118
|
+
if (execution.state === "succeeded") {
|
|
119
|
+
fail("Builder execution already succeeded and cannot be revalidated.", {
|
|
120
|
+
errorCode: "builder_execution_already_succeeded",
|
|
121
|
+
statusCode: 409,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (
|
|
125
|
+
execution.state === "response_received" ||
|
|
126
|
+
execution.state === "reconciliation_required"
|
|
127
|
+
) {
|
|
128
|
+
fail("Builder execution requires reconciliation before validation.", {
|
|
129
|
+
errorCode: "builder_execution_reconciliation_required",
|
|
130
|
+
statusCode: 409,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (
|
|
134
|
+
execution.state === "running" &&
|
|
135
|
+
!isBuilderExecutionClaimStale(execution.updatedAt, now)
|
|
136
|
+
) {
|
|
137
|
+
fail("Builder execution is already running.", {
|
|
138
|
+
errorCode: "builder_execution_running",
|
|
139
|
+
statusCode: 409,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const binding = {
|
|
144
|
+
ownerEmail: execution.ownerEmail,
|
|
145
|
+
sourceId: execution.sourceId,
|
|
146
|
+
changeSetId: execution.changeSetId,
|
|
147
|
+
executionId: execution.id,
|
|
148
|
+
idempotencyKey: execution.idempotencyKey,
|
|
149
|
+
};
|
|
150
|
+
const storedPayload = await readBuilderExecutionPayload({
|
|
151
|
+
payloadJson: execution.payloadJson,
|
|
152
|
+
binding,
|
|
153
|
+
});
|
|
122
154
|
const payload = validateBuilderCmsExecutionDryRun({
|
|
123
|
-
storedPayload
|
|
155
|
+
storedPayload,
|
|
124
156
|
plan,
|
|
125
157
|
now,
|
|
126
158
|
});
|
|
@@ -136,17 +168,94 @@ export default defineAction({
|
|
|
136
168
|
? `${plan.summary} Dry run validated blockers locally.`
|
|
137
169
|
: `${plan.summary} Dry run found a stale execution gate.${stale}`;
|
|
138
170
|
|
|
139
|
-
await
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
171
|
+
const payloadJson = await storeBuilderExecutionPayload({
|
|
172
|
+
payload,
|
|
173
|
+
binding,
|
|
174
|
+
});
|
|
175
|
+
const nextReference = builderExecutionPayloadReference(payloadJson);
|
|
176
|
+
const previousReference = builderExecutionPayloadReference(
|
|
177
|
+
execution.payloadJson,
|
|
178
|
+
);
|
|
179
|
+
try {
|
|
180
|
+
const updatedRows = await db.transaction(async (tx) => {
|
|
181
|
+
const updated = await tx
|
|
182
|
+
.update(schema.contentDatabaseSourceExecutions)
|
|
183
|
+
.set({
|
|
184
|
+
state: dryRun?.status === "stale" ? "blocked" : plan.state,
|
|
185
|
+
summary,
|
|
186
|
+
payloadJson,
|
|
187
|
+
attemptToken: null,
|
|
188
|
+
lastError:
|
|
189
|
+
dryRun?.status === "stale"
|
|
190
|
+
? dryRun.mismatches[0]
|
|
191
|
+
: plan.lastError,
|
|
192
|
+
updatedAt: now,
|
|
193
|
+
})
|
|
194
|
+
.where(
|
|
195
|
+
and(
|
|
196
|
+
eq(schema.contentDatabaseSourceExecutions.id, execution.id),
|
|
197
|
+
eq(
|
|
198
|
+
schema.contentDatabaseSourceExecutions.payloadJson,
|
|
199
|
+
execution.payloadJson,
|
|
200
|
+
),
|
|
201
|
+
eq(schema.contentDatabaseSourceExecutions.state, execution.state),
|
|
202
|
+
execution.attemptToken
|
|
203
|
+
? eq(
|
|
204
|
+
schema.contentDatabaseSourceExecutions.attemptToken,
|
|
205
|
+
execution.attemptToken,
|
|
206
|
+
)
|
|
207
|
+
: isNull(schema.contentDatabaseSourceExecutions.attemptToken),
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
.returning({ id: schema.contentDatabaseSourceExecutions.id });
|
|
211
|
+
if (
|
|
212
|
+
updated[0] &&
|
|
213
|
+
previousReference &&
|
|
214
|
+
previousReference !== nextReference
|
|
215
|
+
) {
|
|
216
|
+
await enqueueBuilderPrivatePayloadCleanup(
|
|
217
|
+
tx as unknown as ReturnType<typeof getDb>,
|
|
218
|
+
previousReference,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return updated;
|
|
222
|
+
});
|
|
223
|
+
const [updated] = updatedRows;
|
|
224
|
+
if (!updated)
|
|
225
|
+
fail("Builder execution changed during validation.", {
|
|
226
|
+
errorCode: "builder_execution_changed",
|
|
227
|
+
statusCode: 409,
|
|
228
|
+
});
|
|
229
|
+
} catch (error) {
|
|
230
|
+
let currentPayloadJson: string | null | undefined;
|
|
231
|
+
try {
|
|
232
|
+
const [current] = await db
|
|
233
|
+
.select({
|
|
234
|
+
payloadJson: schema.contentDatabaseSourceExecutions.payloadJson,
|
|
235
|
+
})
|
|
236
|
+
.from(schema.contentDatabaseSourceExecutions)
|
|
237
|
+
.where(eq(schema.contentDatabaseSourceExecutions.id, execution.id))
|
|
238
|
+
.limit(1);
|
|
239
|
+
currentPayloadJson = current?.payloadJson;
|
|
240
|
+
} catch {
|
|
241
|
+
// Inconclusive readback retains both refs.
|
|
242
|
+
}
|
|
243
|
+
if (currentPayloadJson !== payloadJson) {
|
|
244
|
+
if (currentPayloadJson !== undefined && nextReference) {
|
|
245
|
+
await cleanupBuilderPrivatePayload(
|
|
246
|
+
nextReference,
|
|
247
|
+
"uncommitted validated execution",
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (previousReference && previousReference !== nextReference) {
|
|
254
|
+
await cleanupBuilderPrivatePayload(
|
|
255
|
+
previousReference,
|
|
256
|
+
"superseded validated execution",
|
|
257
|
+
);
|
|
258
|
+
}
|
|
150
259
|
|
|
151
260
|
await db
|
|
152
261
|
.update(schema.contentDatabaseSources)
|