@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.
Files changed (196) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +61 -1
  3. package/corpus/templates/analytics/AGENTS.md +1 -1
  4. package/corpus/templates/analytics/CHANGELOG.md +3 -0
  5. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +9 -65
  6. package/corpus/templates/analytics/actions/list-event-catalog.ts +1 -1
  7. package/corpus/templates/analytics/actions/list-session-outputs.ts +40 -0
  8. package/corpus/templates/analytics/actions/list-session-recordings.ts +17 -4
  9. package/corpus/templates/analytics/actions/view-screen.ts +29 -3
  10. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +38 -167
  11. package/corpus/templates/analytics/app/i18n/en-US.ts +27 -0
  12. package/corpus/templates/analytics/app/pages/sessions/EventCatalogPage.tsx +18 -1
  13. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +3 -1
  14. package/corpus/templates/analytics/app/pages/sessions/SessionOutputs.tsx +197 -0
  15. package/corpus/templates/analytics/app/pages/sessions/SessionsTriagePage.tsx +114 -4
  16. package/corpus/templates/analytics/changelog/2026-10-15-sessions-can-find-people-who-made-a-slides-deck-but-didn-t-p.md +6 -0
  17. package/corpus/templates/analytics/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  18. package/corpus/templates/analytics/changelog/2026-10-16-the-activation-funnel-counts-visitors-who-decline-analytics-.md +6 -0
  19. package/corpus/templates/analytics/server/db/schema.ts +67 -0
  20. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +9 -0
  21. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +28 -22
  22. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +36 -1
  23. package/corpus/templates/analytics/server/lib/output-outcomes.ts +712 -0
  24. package/corpus/templates/analytics/server/lib/replay-origins.ts +33 -0
  25. package/corpus/templates/analytics/server/lib/session-event-index.ts +20 -0
  26. package/corpus/templates/analytics/server/lib/session-performance.ts +6 -40
  27. package/corpus/templates/analytics/server/lib/session-recording-associations.ts +30 -1
  28. package/corpus/templates/analytics/server/lib/session-replay-triage.ts +99 -0
  29. package/corpus/templates/analytics/server/lib/session-replay.ts +45 -31
  30. package/corpus/templates/analytics/server/lib/sessions-triage-lab.ts +8 -2
  31. package/corpus/templates/analytics/server/plugins/db.ts +46 -0
  32. package/corpus/templates/analytics/shared/output-use.ts +205 -0
  33. package/corpus/templates/analytics/shared/session-events.ts +6 -0
  34. package/corpus/templates/assets/CHANGELOG.md +6 -0
  35. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +51 -117
  36. package/corpus/templates/assets/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  37. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +34 -101
  38. package/corpus/templates/brain/app/routes/sources.tsx +1 -1
  39. package/corpus/templates/brain/vitest.config.ts +7 -0
  40. package/corpus/templates/calendar/CHANGELOG.md +12 -0
  41. package/corpus/templates/calendar/actions/create-event.ts +2 -4
  42. package/corpus/templates/calendar/actions/update-event.ts +35 -6
  43. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +1 -1
  44. package/corpus/templates/calendar/changelog/2026-10-16-booking-links-show-localized-visibility-badges.md +6 -0
  45. package/corpus/templates/calendar/changelog/2026-10-21-solo-events-avoid-notification-approval.md +6 -0
  46. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +62 -101
  47. package/corpus/templates/clips/CHANGELOG.md +7 -20
  48. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +20 -77
  49. package/corpus/templates/clips/app/components/player/share-dialog.tsx +63 -90
  50. package/corpus/templates/clips/app/components/sharing/share-test-support.tsx +180 -0
  51. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -584
  52. package/corpus/templates/clips/changelog/2026-10-16-share-dialogs-make-it-easier-to-see-who-has-access-and-manag.md +6 -0
  53. package/corpus/templates/clips/changelog/2026-10-21-share-panels-describe-general-access-in-a-sentence.md +6 -0
  54. package/corpus/templates/community/qr/package.json +1 -1
  55. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -3
  56. package/corpus/templates/content/CHANGELOG.md +10 -0
  57. package/corpus/templates/content/actions/_browser-document-save-attempt.ts +23 -1
  58. package/corpus/templates/content/actions/_builder-cms-blob-custody.ts +467 -0
  59. package/corpus/templates/content/actions/_builder-cms-execution-lease.ts +17 -0
  60. package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +5 -0
  61. package/corpus/templates/content/actions/_builder-cms-read-client.ts +881 -21
  62. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +28 -2
  63. package/corpus/templates/content/actions/_builder-cms-write-client.ts +109 -11
  64. package/corpus/templates/content/actions/_builder-docs-client.ts +1 -0
  65. package/corpus/templates/content/actions/_builder-private-blob.test-fixture.ts +90 -0
  66. package/corpus/templates/content/actions/_builder-source-execution-guard.ts +48 -0
  67. package/corpus/templates/content/actions/_content-save-outcomes.ts +5 -0
  68. package/corpus/templates/content/actions/_database-source-utils.ts +290 -8
  69. package/corpus/templates/content/actions/_database-utils.ts +93 -7
  70. package/corpus/templates/content/actions/_database-view-setup.ts +21 -2
  71. package/corpus/templates/content/actions/_delete-content-space.ts +8 -1
  72. package/corpus/templates/content/actions/_github-source-freshness.ts +30 -4
  73. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -6
  74. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +110 -26
  75. package/corpus/templates/content/actions/change-content-database-source-role.ts +51 -7
  76. package/corpus/templates/content/actions/delete-document.ts +41 -3
  77. package/corpus/templates/content/actions/disconnect-content-database-source.ts +68 -2
  78. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +33 -2
  79. package/corpus/templates/content/actions/execute-builder-source-execution.ts +900 -140
  80. package/corpus/templates/content/actions/get-document.ts +2 -0
  81. package/corpus/templates/content/actions/permanently-delete-document.ts +6 -0
  82. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +155 -13
  83. package/corpus/templates/content/actions/prepare-builder-source-review.ts +299 -73
  84. package/corpus/templates/content/actions/set-live-body-switch.ts +45 -0
  85. package/corpus/templates/content/actions/update-document.ts +139 -20
  86. package/corpus/templates/content/actions/validate-builder-source-execution.ts +135 -26
  87. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +119 -30
  88. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +56 -13
  89. package/corpus/templates/content/app/components/editor/SuggestionText.tsx +18 -3
  90. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +89 -22
  91. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +23 -0
  92. package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +27 -2
  93. package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +32 -8
  94. package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +27 -2
  95. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +1 -0
  96. package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +383 -34
  97. package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +31 -0
  98. package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +82 -4
  99. package/corpus/templates/content/app/global.css +31 -0
  100. package/corpus/templates/content/app/hooks/use-content-database.ts +40 -2
  101. package/corpus/templates/content/app/hooks/use-documents.ts +14 -0
  102. package/corpus/templates/content/app/i18n-data.ts +1 -0
  103. package/corpus/templates/content/changelog/2026-10-16-accepting-a-suggestion-on-a-page-that-changed-since-it-was-s.md +6 -0
  104. package/corpus/templates/content/changelog/2026-10-17-builder-content-avoids-retrying-a-write-when-builder-accepte.md +6 -0
  105. package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +1 -1
  106. package/corpus/templates/content/changelog/2026-10-20-builder-content-sync-keeps-read-and-write-status-accurate-wh.md +6 -0
  107. package/corpus/templates/content/changelog/2026-10-20-suggested-edits-that-add-remove-split-or-merge-paragraphs-li.md +6 -0
  108. package/corpus/templates/content/changelog/2026-10-21-list-rows-can-show-each-pages-first-lines.md +6 -0
  109. package/corpus/templates/content/changelog/2026-10-22-a-collection-linked-to-a-big-github-folder-fills-faster-and-p.md +6 -0
  110. package/corpus/templates/content/docs/product/capabilities/content.source.adapters.md +29 -0
  111. package/corpus/templates/content/parity/matrix.md +42 -42
  112. package/corpus/templates/content/parity/matrix.ts +3 -1
  113. package/corpus/templates/content/server/db/schema.ts +20 -0
  114. package/corpus/templates/content/server/lib/content-trash-purge.ts +15 -1
  115. package/corpus/templates/content/server/lib/live-body-writer.ts +138 -26
  116. package/corpus/templates/content/server/live-copy/binding.ts +31 -0
  117. package/corpus/templates/content/server/live-copy/bounded-task.d.ts +6 -0
  118. package/corpus/templates/content/server/live-copy/exec-db.ts +1 -0
  119. package/corpus/templates/content/server/live-copy/holds.ts +59 -0
  120. package/corpus/templates/content/server/live-copy/persist.ts +656 -0
  121. package/corpus/templates/content/server/live-copy/switch.ts +136 -0
  122. package/corpus/templates/content/server/live-copy/task-names.ts +4 -0
  123. package/corpus/templates/content/server/live-copy/tasks.ts +183 -0
  124. package/corpus/templates/content/server/live-copy/write-cause.ts +85 -0
  125. package/corpus/templates/content/server/live-copy/write.ts +85 -0
  126. package/corpus/templates/content/server/plugins/builder-private-payload-cleanup.ts +22 -0
  127. package/corpus/templates/content/server/plugins/collab.ts +3 -0
  128. package/corpus/templates/content/server/plugins/db.ts +28 -0
  129. package/corpus/templates/content/shared/api.ts +30 -1
  130. package/corpus/templates/content/shared/database-item-layout.ts +54 -22
  131. package/corpus/templates/content/shared/document-intent-merge.ts +16 -9
  132. package/corpus/templates/content/shared/og-description.ts +258 -36
  133. package/corpus/templates/content/shared/suggestion-diff.ts +23 -6
  134. package/corpus/templates/content/shared/suggestion-formatting.ts +44 -14
  135. package/corpus/templates/content/shared/suggestion-text.ts +20 -5
  136. package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +5 -1
  137. package/corpus/templates/design/.generated/bridge/shader-runtime.generated.ts +25 -4
  138. package/corpus/templates/design/CHANGELOG.md +5 -0
  139. package/corpus/templates/design/actions/get-getting-started.ts +53 -0
  140. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +10 -0
  141. package/corpus/templates/design/app/components/design/bridge/native-shader-runtime.bridge.ts +132 -79
  142. package/corpus/templates/design/app/components/design/bridge/native-source-provider.ts +268 -130
  143. package/corpus/templates/design/app/components/design/bridge/native-worker-scene-host.ts +122 -42
  144. package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +41 -4
  145. package/corpus/templates/design/app/components/design/design-canvas/text-content-change-result.ts +46 -12
  146. package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +15 -8
  147. package/corpus/templates/design/app/components/layout/GettingStarted.tsx +50 -0
  148. package/corpus/templates/design/app/components/layout/Sidebar.tsx +3 -0
  149. package/corpus/templates/design/app/components/templates/DesignTemplateGrid.tsx +163 -0
  150. package/corpus/templates/design/app/components/templates/DesignTemplateLibrary.tsx +65 -78
  151. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +6 -2
  152. package/corpus/templates/design/app/global.css +6 -0
  153. package/corpus/templates/design/app/i18n/en-US.ts +17 -0
  154. package/corpus/templates/design/app/lib/canvas-load-outcome.ts +147 -0
  155. package/corpus/templates/design/app/lib/canvas-outcome-tracking.ts +531 -0
  156. package/corpus/templates/design/app/lib/canvas-style-kind.ts +50 -0
  157. package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
  158. package/corpus/templates/design/app/pages/Index.tsx +28 -67
  159. package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +86 -29
  160. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +38 -3
  161. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-core.ts +9 -0
  162. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-files-and-saving.ts +2 -0
  163. package/corpus/templates/design/changelog/2026-10-16-design-pages-use-localized-visibility-badges-and-a-shared-ow.md +6 -0
  164. package/corpus/templates/design/changelog/2026-10-21-a-getting-started-checklist-at-the-bottom-of-the-sidebar-tra.md +6 -0
  165. package/corpus/templates/design/changelog/2026-10-21-templates-now-open-a-read-only-preview-from-the-card-and-the.md +6 -0
  166. package/corpus/templates/design/changelog/2026-10-22-collapsed-shader-layers-no-longer-interrupt-other-shaders-on.md +6 -0
  167. package/corpus/templates/design/changelog/2026-10-22-shader-fills-keep-rendering-when-other-screen-content-cannot.md +6 -0
  168. package/corpus/templates/design/scripts/design-export-validation/export-baseline.json +24 -0
  169. package/corpus/templates/design/server/lib/getting-started.ts +267 -0
  170. package/corpus/templates/design/server/plugins/getting-started.ts +5 -0
  171. package/corpus/templates/design/shared/design-template-presets/analytics-dashboard.ts +127 -0
  172. package/corpus/templates/design/shared/design-template-presets/design-studio.ts +98 -0
  173. package/corpus/templates/design/shared/design-template-presets/messaging-app.ts +101 -0
  174. package/corpus/templates/design/shared/design-template-presets/project-tasks.ts +120 -0
  175. package/corpus/templates/design/shared/design-template-presets.ts +8 -0
  176. package/corpus/templates/dispatch/CHANGELOG.md +4 -0
  177. package/corpus/templates/dispatch/app/i18n-data.ts +75 -0
  178. package/corpus/templates/dispatch/changelog/2026-10-21-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  179. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +34 -100
  180. package/corpus/templates/forms/app/pages/FormsListPage.tsx +1 -1
  181. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
  182. package/corpus/templates/plan/CHANGELOG.md +6 -0
  183. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +34 -109
  184. package/corpus/templates/plan/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  185. package/corpus/templates/slides/CHANGELOG.md +7 -0
  186. package/corpus/templates/slides/actions/delete-deck.ts +7 -0
  187. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
  188. package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +1 -1
  189. package/corpus/templates/slides/app/pages/Index.tsx +7 -2
  190. package/corpus/templates/slides/changelog/2026-10-16-deck-lists-use-the-shared-ownership-filter-and-visibility-ba.md +6 -0
  191. package/corpus/templates/slides/changelog/2026-10-21-connected-deck-deletion-requires-approval.md +6 -0
  192. package/corpus/templates/slides/scripts/edit-fidelity/client-action.ts +61 -0
  193. package/corpus/templates/slides/scripts/edit-fidelity/run.ts +10 -0
  194. package/package.json +1 -1
  195. package/corpus/templates/chat/app/lib/sidebar-thread-state.ts +0 -29
  196. package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +0 -83
@@ -0,0 +1,136 @@
1
+ import type { ActionRunContext } from "@agent-native/core/action";
2
+ import {
3
+ mutateLiveCopy,
4
+ readLiveCopy,
5
+ seedLiveCopy,
6
+ } from "@agent-native/core/collab";
7
+ import { eq } from "drizzle-orm";
8
+
9
+ import { assertSourceAllowsPageEdit } from "../../actions/_document-source.js";
10
+ import { getDb, schema } from "../db/index.js";
11
+ import {
12
+ liveBodyWriterFlagsOn,
13
+ liveBodyWriterGate,
14
+ liveBodyWriterPageColumns,
15
+ type LiveBodyWriterGate,
16
+ } from "../lib/live-body-writer.js";
17
+ import { CONTENT_LIVE_COPY_EDITS, contentLiveCopy } from "./binding.js";
18
+ import {
19
+ LiveBodyConversionError,
20
+ persistLiveBodyConversion,
21
+ } from "./persist.js";
22
+ import { withLiveBodyConversion } from "./write-cause.js";
23
+
24
+ const CONVERSION_ATTEMPTS = 3;
25
+
26
+ async function readPage(documentId: string) {
27
+ // guard:allow-unscoped — set-live-body-switch checks editor access before it switches a page.
28
+ const [page] = await getDb()
29
+ .select({
30
+ ...liveBodyWriterPageColumns,
31
+ content: schema.documents.content,
32
+ sourceMode: schema.documents.sourceMode,
33
+ sourceRootPath: schema.documents.sourceRootPath,
34
+ sourcePath: schema.documents.sourcePath,
35
+ })
36
+ .from(schema.documents)
37
+ .where(eq(schema.documents.id, documentId))
38
+ .limit(1);
39
+ return page;
40
+ }
41
+
42
+ /**
43
+ * Switches a page to its live copy: makes the live copy hold the stored body
44
+ * (seeding it, or editing it to match, so open editors merge rather than
45
+ * reload), then marks the page switched in the same commit. The stored body
46
+ * wins over Yjs-only edits, so switch a page before an editor binds to it.
47
+ * Throws `LiveBodyConversionError` `flag_off` unless both flags are on for
48
+ * the page's owner, and `base_changed` when the body keeps moving.
49
+ */
50
+ export async function switchPageToLiveCopy(
51
+ documentId: string,
52
+ options: { ctx?: ActionRunContext } = {},
53
+ ): Promise<LiveBodyWriterGate> {
54
+ for (let attempt = 0; attempt < CONVERSION_ATTEMPTS; attempt += 1) {
55
+ const page = await readPage(documentId);
56
+ if (!page) return { writePath: "legacy", reason: "no_page" };
57
+ assertSourceAllowsPageEdit(page);
58
+ const gate = await liveBodyWriterGate(documentId);
59
+ if (gate.writePath === "live_copy") return gate;
60
+ if (!(await liveBodyWriterFlagsOn(page))) {
61
+ throw new LiveBodyConversionError(
62
+ "flag_off",
63
+ documentId,
64
+ `The live-copy writer is off for page ${documentId}'s owner.`,
65
+ );
66
+ }
67
+ const conversion = { documentId, baseRevision: page.bodyRevision };
68
+ try {
69
+ await withLiveBodyConversion(
70
+ conversion,
71
+ { ctx: options.ctx, operation: "switch-live-body" },
72
+ async () => {
73
+ const live = await readLiveCopy(contentLiveCopy, documentId);
74
+ if (live.kind === "live") {
75
+ await mutateLiveCopy(contentLiveCopy, documentId, {
76
+ edit: CONTENT_LIVE_COPY_EDITS.replaceBody,
77
+ input: { content: page.content },
78
+ // An edit that changes nothing commits no live copy, so the
79
+ // binding's persist does not run; switch the page here.
80
+ persist: async (tx, change) => {
81
+ if (!change.changed) {
82
+ await persistLiveBodyConversion(
83
+ tx,
84
+ conversion,
85
+ change.text,
86
+ change.version,
87
+ );
88
+ }
89
+ },
90
+ });
91
+ return;
92
+ }
93
+ const seeded = await seedLiveCopy(contentLiveCopy, documentId, {
94
+ source: page.content,
95
+ });
96
+ if (!seeded.seeded) {
97
+ throw new LiveBodyConversionError(
98
+ "base_changed",
99
+ documentId,
100
+ `Page ${documentId}'s live copy was seeded while it was being switched.`,
101
+ );
102
+ }
103
+ },
104
+ );
105
+ } catch (error) {
106
+ if (
107
+ error instanceof LiveBodyConversionError &&
108
+ error.code === "base_changed"
109
+ ) {
110
+ continue;
111
+ }
112
+ throw error;
113
+ }
114
+ return liveBodyWriterGate(documentId);
115
+ }
116
+ throw new LiveBodyConversionError(
117
+ "base_changed",
118
+ documentId,
119
+ `Page ${documentId} kept changing while it was being switched to its live copy.`,
120
+ );
121
+ }
122
+
123
+ /**
124
+ * Sends a page's body writes back to the legacy path. Its live copy stays;
125
+ * the first legacy body write leaves it behind, so switching back converts
126
+ * the page again.
127
+ */
128
+ export async function switchPageToLegacy(
129
+ documentId: string,
130
+ ): Promise<LiveBodyWriterGate> {
131
+ await getDb()
132
+ .update(schema.documents)
133
+ .set({ liveBodySwitchedAt: null })
134
+ .where(eq(schema.documents.id, documentId));
135
+ return liveBodyWriterGate(documentId);
136
+ }
@@ -0,0 +1,4 @@
1
+ /** Content's own tasks in the live copy's bounded worker, beside core's. */
2
+ export const CONTENT_HOLDS_BODY_TASK = "content.holdsBody";
3
+
4
+ export type HoldsBodyResult = "held" | "unheld" | "noncanonical";
@@ -0,0 +1,183 @@
1
+ import {
2
+ defineLiveCopyTasks,
3
+ type LiveCopyTaskRefusal,
4
+ } from "@agent-native/core/collab/live-copy-tasks";
5
+ import type { Node as PMNode, Schema } from "@tiptap/pm/model";
6
+ import {
7
+ initProseMirrorDoc,
8
+ prosemirrorToYXmlFragment,
9
+ updateYFragment,
10
+ } from "@tiptap/y-tiptap";
11
+ import * as Y from "yjs";
12
+
13
+ import { createContentEditorStructuralSchema } from "../../shared/content-editor-structural-schema.js";
14
+ import { bodyHoldsChanges } from "../../shared/document-intent-merge.js";
15
+ import {
16
+ LIVE_BODY_FIELD,
17
+ materializeLiveBody,
18
+ } from "../../shared/live-body.js";
19
+ import { docToNfm, nfmToDoc, parseNfmWithLosses } from "../../shared/nfm.js";
20
+ import { CONTENT_HOLDS_BODY_TASK, type HoldsBodyResult } from "./task-names.js";
21
+
22
+ // The worker half of Content's live copy: every handler here runs in the
23
+ // bounded task runner, so import only what a worker bundle can carry.
24
+
25
+ let structuralSchema: Schema | undefined;
26
+
27
+ function schema(): Schema {
28
+ structuralSchema ??= createContentEditorStructuralSchema();
29
+ return structuralSchema;
30
+ }
31
+
32
+ function refused(reason: string, detail?: string): LiveCopyTaskRefusal {
33
+ return { kind: "refused", reason, ...(detail ? { detail } : {}) };
34
+ }
35
+
36
+ function assertBodyField(field: string): void {
37
+ if (field !== LIVE_BODY_FIELD) {
38
+ throw new Error(
39
+ `Content's live copy binds "${LIVE_BODY_FIELD}", not "${field}".`,
40
+ );
41
+ }
42
+ }
43
+
44
+ function deriveBody(doc: Y.Doc, field: string): string | LiveCopyTaskRefusal {
45
+ assertBodyField(field);
46
+ // Building the tree deletes what the schema rejects, and derive must not
47
+ // edit the doc it is given.
48
+ const copy = new Y.Doc();
49
+ try {
50
+ Y.applyUpdate(copy, Y.encodeStateAsUpdate(doc));
51
+ const body = materializeLiveBody(copy);
52
+ if (body.kind === "lossy") {
53
+ return refused("lossy", "the page schema cannot carry part of the body");
54
+ }
55
+ if (body.kind === "uninitialized") return refused("empty");
56
+ return body.markdown;
57
+ } finally {
58
+ copy.destroy();
59
+ }
60
+ }
61
+
62
+ /**
63
+ * The tree a Markdown body becomes in the editor. A body the parser or the
64
+ * schema cannot keep in full is refused: the live copy would keep the rest
65
+ * and the page would lose that part on its next commit.
66
+ */
67
+ function buildBody(markdown: string): PMNode | LiveCopyTaskRefusal {
68
+ // What the parser dropped is in neither serialization compared below.
69
+ const { doc: parsed, losses } = parseNfmWithLosses(markdown);
70
+ if (losses.length > 0) {
71
+ const kinds = [...new Set(losses.map((loss) => loss.kind))].join(", ");
72
+ return refused("lossy", `the parser drops part of this body (${kinds})`);
73
+ }
74
+ let root: PMNode;
75
+ try {
76
+ root = schema().nodeFromJSON(parsed);
77
+ root.check();
78
+ } catch (error) {
79
+ return refused(
80
+ "unrepresentable",
81
+ error instanceof Error ? error.message : undefined,
82
+ );
83
+ }
84
+ if (docToNfm(root.toJSON()) !== docToNfm(parsed)) {
85
+ return refused("lossy", "the page schema drops part of this body");
86
+ }
87
+ return root;
88
+ }
89
+
90
+ function replaceBodyInput(
91
+ input: unknown,
92
+ ): { content: string; expected?: string } | LiveCopyTaskRefusal {
93
+ if (typeof input !== "object" || input === null) {
94
+ return refused("invalid_input", "expected { content, expected? }");
95
+ }
96
+ const { content, expected } = input as Record<string, unknown>;
97
+ if (typeof content !== "string") {
98
+ return refused("invalid_input", "content must be a string");
99
+ }
100
+ if (expected !== undefined && typeof expected !== "string") {
101
+ return refused("invalid_input", "expected must be a string");
102
+ }
103
+ return expected === undefined ? { content } : { content, expected };
104
+ }
105
+
106
+ function isRefusal(value: unknown): value is LiveCopyTaskRefusal {
107
+ return (
108
+ typeof value === "object" &&
109
+ value !== null &&
110
+ (value as { kind?: unknown }).kind === "refused"
111
+ );
112
+ }
113
+
114
+ function isCanonical(markdown: string): boolean {
115
+ try {
116
+ return docToNfm(nfmToDoc(markdown)) === markdown;
117
+ } catch {
118
+ // coercion-ok: a body the parser rejects is not canonical, and its save keeps its draft.
119
+ return false;
120
+ }
121
+ }
122
+
123
+ function holdsBodyInput(input: unknown): {
124
+ base: string;
125
+ stored: string;
126
+ candidate: string;
127
+ } {
128
+ const { base, stored, candidate } = (input ?? {}) as Record<string, unknown>;
129
+ if (
130
+ typeof base !== "string" ||
131
+ typeof stored !== "string" ||
132
+ typeof candidate !== "string"
133
+ ) {
134
+ throw new TypeError("expected { base, stored, candidate } as strings");
135
+ }
136
+ return { base, stored, candidate };
137
+ }
138
+
139
+ const liveCopyTasks = defineLiveCopyTasks({
140
+ derive: deriveBody,
141
+
142
+ seed(doc, field, source) {
143
+ assertBodyField(field);
144
+ const root = buildBody(source);
145
+ if (isRefusal(root)) return root;
146
+ prosemirrorToYXmlFragment(root, doc.getXmlFragment(field));
147
+ },
148
+
149
+ edits: {
150
+ /**
151
+ * Makes the body `content`, editing only the blocks that differ so open
152
+ * editors keep their place. With `expected`, refuses `base_changed`
153
+ * unless the current body is exactly that text.
154
+ */
155
+ "replace-body"(doc, field, input) {
156
+ const parsed = replaceBodyInput(input);
157
+ if (isRefusal(parsed)) return parsed;
158
+ if (parsed.expected !== undefined) {
159
+ const current = deriveBody(doc, field);
160
+ if (isRefusal(current)) return current;
161
+ if (current !== parsed.expected) return refused("base_changed");
162
+ }
163
+ const root = buildBody(parsed.content);
164
+ if (isRefusal(root)) return root;
165
+ const fragment = doc.getXmlFragment(field);
166
+ const { meta } = initProseMirrorDoc(fragment, schema());
167
+ updateYFragment(doc, fragment, root, meta);
168
+ },
169
+ },
170
+ });
171
+
172
+ export const tasks = {
173
+ ...liveCopyTasks,
174
+ /** Whether the stored body holds every change `candidate` made to `base`. */
175
+ async [CONTENT_HOLDS_BODY_TASK](input: unknown): Promise<HoldsBodyResult> {
176
+ const { base, stored, candidate } = holdsBodyInput(input);
177
+ // The comparison reads the candidate in its parsed form, and a parse can
178
+ // drop text, such as text directly inside a table. Only an editor's own
179
+ // serialization says what the save holds.
180
+ if (!isCanonical(candidate)) return "noncanonical";
181
+ return bodyHoldsChanges(base, stored, candidate) ? "held" : "unheld";
182
+ },
183
+ };
@@ -0,0 +1,85 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ import type { ActionRunContext } from "@agent-native/core/action";
4
+
5
+ import type { DocumentBodyIntent } from "../../shared/document-intent-order.js";
6
+
7
+ export interface ContentBodyIntentRecord {
8
+ /**
9
+ * Without `authoredBaseRevision`, persist records the revision the edit
10
+ * was applied to: the page's, under its lock.
11
+ */
12
+ intent: Omit<DocumentBodyIntent, "authoredBaseRevision"> & {
13
+ authoredBaseRevision?: number;
14
+ };
15
+ /**
16
+ * For a writer that knows its result only once the edit runs: without
17
+ * these, persist takes the committed body's hash and the blocks it changed.
18
+ */
19
+ candidateHash?: string;
20
+ metadataHash?: string;
21
+ changedBlockIndexes?: number[];
22
+ displacedCheckpointId?: string;
23
+ }
24
+
25
+ /**
26
+ * Why a server write changed a page body. A live-copy commit's `persist`
27
+ * records it: a History transition for every commit, and the body intent once
28
+ * the commit's revision is known. Server writers pass it to
29
+ * `editContentLiveCopy`, which also records an unchanged edit's intent; core
30
+ * runs no binding persist for one. Give each edit its own cause; an intent
31
+ * recorded twice would claim two revisions.
32
+ */
33
+ export interface ContentBodyWriteCause {
34
+ ctx?: ActionRunContext;
35
+ /** The History operation, such as `"accept-suggestion"`. */
36
+ operation: string;
37
+ origin?: string;
38
+ historySessionId?: string;
39
+ intent?: ContentBodyIntentRecord;
40
+ }
41
+
42
+ /** A page being switched to its live copy, from the body at `baseRevision`. */
43
+ export interface LiveBodyConversion {
44
+ documentId: string;
45
+ baseRevision: number;
46
+ }
47
+
48
+ interface WriteScope {
49
+ cause: ContentBodyWriteCause;
50
+ conversion?: LiveBodyConversion;
51
+ }
52
+
53
+ const scopes = new AsyncLocalStorage<WriteScope>();
54
+
55
+ export function withContentBodyWriteCause<T>(
56
+ cause: ContentBodyWriteCause,
57
+ run: () => Promise<T>,
58
+ ): Promise<T> {
59
+ return scopes.run({ cause }, run);
60
+ }
61
+
62
+ export function currentContentBodyWriteCause():
63
+ | ContentBodyWriteCause
64
+ | undefined {
65
+ return scopes.getStore()?.cause;
66
+ }
67
+
68
+ /**
69
+ * Runs a conversion. The page is not switched until its commit lands, so the
70
+ * binding treats `conversion.documentId` as a live copy only inside `run`.
71
+ */
72
+ export function withLiveBodyConversion<T>(
73
+ conversion: LiveBodyConversion,
74
+ cause: ContentBodyWriteCause,
75
+ run: () => Promise<T>,
76
+ ): Promise<T> {
77
+ return scopes.run({ cause, conversion }, run);
78
+ }
79
+
80
+ export function liveBodyConversionFor(
81
+ documentId: string,
82
+ ): LiveBodyConversion | undefined {
83
+ const conversion = scopes.getStore()?.conversion;
84
+ return conversion?.documentId === documentId ? conversion : undefined;
85
+ }
@@ -0,0 +1,85 @@
1
+ import {
2
+ mutateLiveCopy,
3
+ type LiveCopyCallChange,
4
+ type LiveCopyMutationResult,
5
+ } from "@agent-native/core/collab";
6
+ import type { DbExec } from "@agent-native/core/db";
7
+
8
+ import { CONTENT_LIVE_COPY_EDITS, contentLiveCopy } from "./binding.js";
9
+ import { persistUnchangedContentBodyEdit } from "./persist.js";
10
+ import {
11
+ withContentBodyWriteCause,
12
+ type ContentBodyWriteCause,
13
+ } from "./write-cause.js";
14
+
15
+ /**
16
+ * A server writer's replace names the body it was built from. Core retries
17
+ * an edit a peer's commit overtook, and a replace without `expected` would
18
+ * silently overwrite the peer's text on that retry.
19
+ */
20
+ type ContentLiveCopyEditInput<Edit extends string> =
21
+ typeof CONTENT_LIVE_COPY_EDITS.replaceBody extends Edit
22
+ ? { content: string; expected: string }
23
+ : unknown;
24
+
25
+ function replaceBodyInput(
26
+ documentId: string,
27
+ input: unknown,
28
+ ): { content: unknown; expected: string } {
29
+ const { content, expected } = (input ?? {}) as {
30
+ content?: unknown;
31
+ expected?: unknown;
32
+ };
33
+ if (typeof expected !== "string") {
34
+ throw new Error(
35
+ `A server writer's replace of page ${documentId} must name the body it was built from as \`expected\`.`,
36
+ );
37
+ }
38
+ // The worker gets a structured clone, which drops a getter's or a
39
+ // non-enumerable property's value, so send the values checked here.
40
+ return { content, expected };
41
+ }
42
+
43
+ /**
44
+ * Applies a named edit to a switched page's live copy for a server writer.
45
+ * A changed edit commits through the binding's persist; an unchanged one
46
+ * still locks the page in Content's order and records the cause's intent,
47
+ * which core's binding persist never sees. `persist` runs after either, in
48
+ * the same transaction, with the page locked.
49
+ *
50
+ * Throws `LIVE_BODY_WRITE_PATH_CHANGED` when the page left its live copy
51
+ * after the writer chose it; nothing commits, so the writer can take its
52
+ * legacy path. An edit a peer's commit overtook is retried against the
53
+ * peer's body, so a replace whose `expected` body changed is refused
54
+ * `base_changed`.
55
+ */
56
+ export async function editContentLiveCopy<Edit extends string>(
57
+ documentId: string,
58
+ options: {
59
+ cause: ContentBodyWriteCause;
60
+ edit: Edit;
61
+ input: ContentLiveCopyEditInput<Edit>;
62
+ requestSource?: string;
63
+ persist?: (tx: DbExec, change: LiveCopyCallChange) => Promise<void>;
64
+ },
65
+ ): Promise<LiveCopyMutationResult> {
66
+ const input =
67
+ options.edit === CONTENT_LIVE_COPY_EDITS.replaceBody
68
+ ? replaceBodyInput(documentId, options.input)
69
+ : options.input;
70
+ return withContentBodyWriteCause(options.cause, () =>
71
+ mutateLiveCopy(contentLiveCopy, documentId, {
72
+ edit: options.edit,
73
+ input,
74
+ ...(options.requestSource
75
+ ? { requestSource: options.requestSource }
76
+ : {}),
77
+ persist: async (tx, change) => {
78
+ if (!change.changed) {
79
+ await persistUnchangedContentBodyEdit(tx, documentId, options.cause);
80
+ }
81
+ await options.persist?.(tx, change);
82
+ },
83
+ }),
84
+ );
85
+ }
@@ -0,0 +1,22 @@
1
+ import {
2
+ defineNitroPlugin,
3
+ registerRecurringSweepHandler,
4
+ } from "@agent-native/core/server";
5
+
6
+ import { drainBuilderPrivatePayloadCleanup } from "../../actions/_builder-cms-blob-custody.js";
7
+
8
+ export const BUILDER_PRIVATE_PAYLOAD_CLEANUP_SWEEP_ID =
9
+ "content-builder-private-payload-cleanup";
10
+
11
+ export function registerBuilderPrivatePayloadCleanupSweep() {
12
+ return registerRecurringSweepHandler(
13
+ BUILDER_PRIVATE_PAYLOAD_CLEANUP_SWEEP_ID,
14
+ async ({ deadlineAt, signal }) => {
15
+ await drainBuilderPrivatePayloadCleanup({ deadlineAt, signal });
16
+ },
17
+ );
18
+ }
19
+
20
+ export default defineNitroPlugin(() => {
21
+ registerBuilderPrivatePayloadCleanupSweep();
22
+ });
@@ -1,9 +1,12 @@
1
1
  import { createCollabPlugin } from "@agent-native/core/server";
2
2
 
3
+ import { contentLiveCopy } from "../live-copy/binding.js";
4
+
3
5
  export default createCollabPlugin({
4
6
  table: "documents",
5
7
  contentColumn: "content",
6
8
  idColumn: "id",
7
9
  autoSeed: false, // Seeding happens via edit-document action, not on startup
8
10
  access: { mode: "resource", resourceType: "document" },
11
+ liveCopy: contentLiveCopy,
9
12
  });
@@ -1479,6 +1479,34 @@ export const runContentMigrations = runMigrations(
1479
1479
  name: "content-live-body-switch",
1480
1480
  sql: `ALTER TABLE documents ADD COLUMN IF NOT EXISTS live_body_switched_at TEXT`,
1481
1481
  },
1482
+ {
1483
+ version: 125,
1484
+ name: "content-live-copy-version",
1485
+ sql: `ALTER TABLE documents ADD COLUMN IF NOT EXISTS live_copy_version INTEGER`,
1486
+ },
1487
+ {
1488
+ version: 126,
1489
+ name: "builder-private-payload-cleanup-queue",
1490
+ sql: `ALTER TABLE document_comments ADD COLUMN IF NOT EXISTS property_id TEXT;
1491
+ ALTER TABLE document_comments ADD COLUMN IF NOT EXISTS property_name TEXT;
1492
+ ALTER TABLE document_comments ADD COLUMN IF NOT EXISTS property_value TEXT;
1493
+ CREATE INDEX IF NOT EXISTS document_comments_document_idx
1494
+ ON document_comments (document_id, owner_email);
1495
+ CREATE TABLE IF NOT EXISTS content_database_builder_blob_cleanup (
1496
+ blob_reference TEXT PRIMARY KEY,
1497
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
1498
+ )`,
1499
+ },
1500
+ {
1501
+ version: 127,
1502
+ name: "builder-private-payload-cleanup-retries",
1503
+ sql: `ALTER TABLE content_database_builder_blob_cleanup
1504
+ ADD COLUMN IF NOT EXISTS attempt_count INTEGER NOT NULL DEFAULT 0;
1505
+ ALTER TABLE content_database_builder_blob_cleanup
1506
+ ADD COLUMN IF NOT EXISTS next_attempt_at TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z';
1507
+ CREATE INDEX IF NOT EXISTS content_database_builder_blob_cleanup_due_idx
1508
+ ON content_database_builder_blob_cleanup (next_attempt_at, created_at)`,
1509
+ },
1482
1510
  ],
1483
1511
  { table: "content_migrations" },
1484
1512
  );
@@ -161,9 +161,23 @@ export interface DocumentUpdateRequest {
161
161
  loadedContentWasEmpty?: boolean;
162
162
  }
163
163
 
164
+ /**
165
+ * The body part of a browser save on a page whose body is its live copy. The
166
+ * body was not written: the editor's updates reach the live copy over collab.
167
+ * `matchesLiveCopy` says whether every submitted, authored and snapshot body
168
+ * equals the body derived from the live copy at `collabVersion`.
169
+ */
170
+ export interface LiveCopyBodySave {
171
+ status: "live_copy";
172
+ matchesLiveCopy: boolean;
173
+ bodyRevision: number;
174
+ collabVersion: number;
175
+ }
176
+
164
177
  export interface DocumentUpdateResponse extends Document {
165
178
  urlPath: string;
166
179
  softDeletedDatabaseIds: string[];
180
+ bodySave?: LiveCopyBodySave;
167
181
  }
168
182
 
169
183
  export interface DocumentMoveRequest {
@@ -461,12 +475,19 @@ export type ContentDatabaseItemLeadingKind =
461
475
  | "checkbox"
462
476
  | "status";
463
477
  export type ContentDatabaseItemTitleWeight = "regular" | "medium" | "bold";
478
+ /** What line 2 shows: a property, or the first lines of each row's page. */
479
+ export type ContentDatabaseItemLine2Source = "property" | "page";
464
480
 
465
481
  /** Which property fills each part of a list row. */
466
482
  export interface ContentDatabaseItemLayout {
467
483
  leading: { kind: ContentDatabaseItemLeadingKind; propertyId: string | null };
468
484
  titleWeight: ContentDatabaseItemTitleWeight;
469
- line2: { propertyId: string | null; lines: 1 | 2 | 3 };
485
+ /** A saved layout leaves `source` out when line 2 shows a property. */
486
+ line2: {
487
+ source?: ContentDatabaseItemLine2Source;
488
+ propertyId: string | null;
489
+ lines: 1 | 2 | 3;
490
+ };
470
491
  line3: string[];
471
492
  trailing: string[];
472
493
  }
@@ -612,6 +633,11 @@ export interface ContentDatabaseItem {
612
633
  properties: DocumentProperty[];
613
634
  workspaceFilesDatabaseId?: string | null;
614
635
  bodyHydration?: ContentDatabaseBodyHydration;
636
+ /**
637
+ * The first lines of the row's page as plain text. List reads include it
638
+ * only when a List view of the collection shows page text in line 2.
639
+ */
640
+ excerpt?: string;
615
641
  sourceRecord?: ContentDatabaseSourceRow;
616
642
  canonicalKey?: string | null;
617
643
  sourceOverlays?: ContentDatabaseSourceOverlay[];
@@ -891,6 +917,7 @@ export interface ContentDatabaseSourceExecution {
891
917
  idempotencyKey: string;
892
918
  summary: string;
893
919
  payload: Record<string, unknown>;
920
+ payloadUnavailable?: true;
894
921
  lastError: string | null;
895
922
  createdAt: string;
896
923
  updatedAt: string;
@@ -977,6 +1004,7 @@ export interface ContentDatabaseSource {
977
1004
  readMode?: "fixture" | "builder-api" | (string & {}) | null;
978
1005
  connectionId?: string | null;
979
1006
  connectionLabel?: string | null;
1007
+ builderSpacePublicKey?: string | null;
980
1008
  truthPolicy?: ContentDatabaseSourceTruthPolicy;
981
1009
  syncPolicy?: "manual" | "keep_in_sync";
982
1010
  liveBridgeEnabled?: boolean;
@@ -986,6 +1014,7 @@ export interface ContentDatabaseSource {
986
1014
  lastReadMatchedRowCount?: number;
987
1015
  lastReadLimit?: number;
988
1016
  lastReadFetchedEntryCount?: number;
1017
+ lastReadCollectionCount?: number;
989
1018
  lastReadPartial?: boolean;
990
1019
  lastReadHasMore?: boolean;
991
1020
  lastReadNextOffset?: number;