@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
@@ -7,6 +7,7 @@ time from the command menu (Cmd+K → "What's new").
7
7
 
8
8
  ### Improved
9
9
 
10
+ - A collection linked to a big GitHub folder fills faster, and pushes to other linked collections aren't held up while it does
10
11
  - Accept remaining and Reject remaining show the result for every suggested edit at once
11
12
  - Accepting a group of suggested edits saves faster and adds one page version
12
13
 
@@ -18,6 +19,10 @@ time from the command menu (Cmd+K → "What's new").
18
19
 
19
20
  ## 2026-10-21
20
21
 
22
+ ### Added
23
+
24
+ - List rows can show the first lines of each page under its title: pick **Page text** for Line 2
25
+
21
26
  ### Improved
22
27
 
23
28
  - Suggested edits show an agent's whole reason: open a group to read past the first two lines, and lone edits show the reason above the change
@@ -37,6 +42,8 @@ time from the command menu (Cmd+K → "What's new").
37
42
 
38
43
  ### Fixed
39
44
 
45
+ - Suggested edits that add, remove, split, or merge paragraphs, line breaks, and list items now show where the break goes and save as one clear suggestion
46
+ - Builder content sync keeps read and write status accurate when API responses fail.
40
47
  - HTML table cells preserve a trailing backslash when opening Markdown.
41
48
  - Preserve empty paragraph colors and refuse unsupported inline image and oversized math formatting before content is stored.
42
49
 
@@ -67,6 +74,7 @@ time from the command menu (Cmd+K → "What's new").
67
74
 
68
75
  ### Added
69
76
 
77
+ - Fill a collection from a GitHub folder through its Sources settings, which an empty collection links to from under its title and which say up front when GitHub can't link and what to do instead. A short preview comes before linking, the collection shows where its pages come from and when they last updated, with a link back to its folder in Sources, and unlinking asks first
70
78
  - Connected AI assistants can make a collection from a GitHub folder, or link an empty one: each Markdown file becomes a view-only page, and a sync brings in the latest pushes
71
79
  - Make a collection from a GitHub folder in the sidebar's New menu, or fill an empty one from its Sources settings, which say up front when GitHub can't link and what to do instead. A short preview comes before linking, the collection shows where its pages come from and when they last updated, and unlinking asks first
72
80
  - Connected AI assistants can link a new collection to a GitHub folder: each Markdown file becomes a view-only page, and a sync brings in the latest pushes
@@ -91,6 +99,7 @@ time from the command menu (Cmd+K → "What's new").
91
99
 
92
100
  ### Fixed
93
101
 
102
+ - Builder Content avoids retrying a write when Builder accepted it but the response could not be saved.
94
103
  - Agents reading a page you have open no longer wait or fail when your edits are already saved
95
104
 
96
105
  ## 2026-10-16
@@ -101,6 +110,7 @@ time from the command menu (Cmd+K → "What's new").
101
110
 
102
111
  ### Fixed
103
112
 
113
+ - Accepting a suggestion on a Page that changed since it was suggested no longer shows "Something went wrong"
104
114
  - A page open in a tab that shows it's in the trash reopens within seconds when someone restores it.
105
115
  - A page restored from Trash returns to its place in its collection, and edits to a page in Trash are refused
106
116
  - Agent-written Markdown keeps nested lists indented with spaces, underscore bold and italics, and escaped punctuation
@@ -4,13 +4,15 @@ import { ActionContractError } from "@agent-native/core";
4
4
  import { and, eq } from "drizzle-orm";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
7
+ import type { LiveCopyBodySave } from "../shared/api.js";
7
8
 
8
9
  export interface BrowserSaveAttemptConfirmation {
9
10
  attemptId: string;
10
- result: "applied" | "replayed";
11
+ result: "applied" | "replayed" | "live_copy";
11
12
  revision: string;
12
13
  updatedAt: string;
13
14
  softDeletedDatabaseIds?: string[];
15
+ bodySave?: LiveCopyBodySave;
14
16
  }
15
17
 
16
18
  export interface BrowserSaveAttemptPreservation {
@@ -72,6 +74,7 @@ export function readBrowserSaveAttempt(
72
74
  reason?: unknown;
73
75
  checkpointId?: unknown;
74
76
  softDeletedDatabaseIds?: unknown;
77
+ bodySave?: unknown;
75
78
  };
76
79
  if (
77
80
  typeof result.revision !== "string" ||
@@ -119,6 +122,12 @@ export function readBrowserSaveAttempt(
119
122
  { errorCode: "BROWSER_SAVE_RECEIPT_INVALID", statusCode: 500 },
120
123
  );
121
124
  }
125
+ if (result.bodySave !== undefined && !isLiveCopyBodySave(result.bodySave)) {
126
+ throw new ActionContractError(
127
+ "The stored browser save receipt has an invalid live-copy body save.",
128
+ { errorCode: "BROWSER_SAVE_RECEIPT_INVALID", statusCode: 500 },
129
+ );
130
+ }
122
131
  return {
123
132
  attemptId: stored.attemptId,
124
133
  result: "replayed",
@@ -127,5 +136,18 @@ export function readBrowserSaveAttempt(
127
136
  softDeletedDatabaseIds: result.softDeletedDatabaseIds as
128
137
  | string[]
129
138
  | undefined,
139
+ ...(result.bodySave ? { bodySave: result.bodySave } : {}),
130
140
  };
131
141
  }
142
+
143
+ function isLiveCopyBodySave(value: unknown): value is LiveCopyBodySave {
144
+ const save = value as Partial<LiveCopyBodySave> | null;
145
+ return (
146
+ typeof save === "object" &&
147
+ save !== null &&
148
+ save.status === "live_copy" &&
149
+ typeof save.matchesLiveCopy === "boolean" &&
150
+ Number.isSafeInteger(save.bodyRevision) &&
151
+ Number.isSafeInteger(save.collabVersion)
152
+ );
153
+ }
@@ -0,0 +1,467 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+
3
+ import { ActionContractError } from "@agent-native/core";
4
+ import {
5
+ deletePrivateBlob,
6
+ putPrivateBlob,
7
+ readPrivateBlob,
8
+ type PrivateBlobHandle,
9
+ } from "@agent-native/core/private-blob";
10
+ import { asc, eq, lte } from "drizzle-orm";
11
+
12
+ import { getDb, schema } from "../server/db/index.js";
13
+
14
+ const textEncoder = new TextEncoder();
15
+ const textDecoder = new TextDecoder();
16
+ const BUILDER_PRIVATE_PAYLOAD_CLEANUP_BATCH_SIZE = 50;
17
+ const BUILDER_PRIVATE_PAYLOAD_CLEANUP_RETRY_BASE_MS = 30_000;
18
+ const BUILDER_PRIVATE_PAYLOAD_CLEANUP_RETRY_MAX_MS = 6 * 60 * 60 * 1000;
19
+
20
+ export const BUILDER_EXECUTION_PAYLOAD_BLOB_KEY =
21
+ "__builder.execution.privatePayload";
22
+
23
+ type BuilderBlobBinding = Record<string, string>;
24
+
25
+ interface BuilderBlobReference {
26
+ kind: "agent-native.builder-private-payload";
27
+ version: 1;
28
+ binding: BuilderBlobBinding;
29
+ sha256: string;
30
+ handle: PrivateBlobHandle;
31
+ }
32
+
33
+ interface BuilderBlobEnvelope<T> {
34
+ kind: "agent-native.builder-private-payload";
35
+ version: 1;
36
+ binding: BuilderBlobBinding;
37
+ payload: T;
38
+ }
39
+
40
+ function stableJson(value: unknown): string {
41
+ if (Array.isArray(value)) {
42
+ return `[${value.map(stableJson).join(",")}]`;
43
+ }
44
+ if (value && typeof value === "object") {
45
+ const record = value as Record<string, unknown>;
46
+ return `{${Object.keys(record)
47
+ .sort()
48
+ .map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
49
+ .join(",")}}`;
50
+ }
51
+ return JSON.stringify(value) ?? "null";
52
+ }
53
+
54
+ function digest(bytes: Uint8Array) {
55
+ return createHash("sha256").update(bytes).digest("hex");
56
+ }
57
+
58
+ export function builderPrivatePayloadUnavailable(
59
+ message: string,
60
+ _cause?: unknown,
61
+ ): never {
62
+ throw new ActionContractError(message, {
63
+ errorCode: "BUILDER_PRIVATE_PAYLOAD_UNAVAILABLE",
64
+ statusCode: 409,
65
+ });
66
+ }
67
+
68
+ function logBuilderPrivatePayloadCleanupFailure(
69
+ label: string,
70
+ handle: PrivateBlobHandle,
71
+ reason: string,
72
+ ) {
73
+ console.error(`[builder-private-payload] ${label} cleanup failed`, {
74
+ provider: handle.provider,
75
+ handleId: handle.id,
76
+ reason,
77
+ });
78
+ }
79
+
80
+ function parseReference(value: unknown): BuilderBlobReference | null {
81
+ if (typeof value !== "string" || !value.trim()) return null;
82
+ try {
83
+ const parsed = JSON.parse(value) as BuilderBlobReference;
84
+ if (
85
+ parsed?.kind !== "agent-native.builder-private-payload" ||
86
+ parsed.version !== 1 ||
87
+ !parsed.binding ||
88
+ typeof parsed.binding !== "object" ||
89
+ typeof parsed.sha256 !== "string" ||
90
+ !parsed.handle ||
91
+ typeof parsed.handle.id !== "string" ||
92
+ typeof parsed.handle.provider !== "string" ||
93
+ parsed.handle.opaque !== true ||
94
+ typeof parsed.handle.encrypted !== "boolean"
95
+ ) {
96
+ return null;
97
+ }
98
+ return parsed;
99
+ // coercion-ok: null is the explicit invalid-reference marker; reads reject it and cleanup never authorizes deletion from it.
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ function sameBinding(actual: BuilderBlobBinding, expected: BuilderBlobBinding) {
106
+ return stableJson(actual) === stableJson(expected);
107
+ }
108
+
109
+ export async function readBuilderPrivatePayload<T>(args: {
110
+ reference: unknown;
111
+ binding: BuilderBlobBinding;
112
+ label: string;
113
+ }): Promise<T> {
114
+ const reference = parseReference(args.reference);
115
+ if (!reference)
116
+ builderPrivatePayloadUnavailable(
117
+ `${args.label} private blob reference is missing or malformed.`,
118
+ );
119
+ if (!sameBinding(reference.binding, args.binding)) {
120
+ builderPrivatePayloadUnavailable(
121
+ `${args.label} private blob reference belongs to a different Builder operation.`,
122
+ );
123
+ }
124
+ let result;
125
+ try {
126
+ result = await readPrivateBlob(reference.handle);
127
+ } catch (error) {
128
+ builderPrivatePayloadUnavailable(
129
+ `${args.label} private blob could not be read.`,
130
+ error,
131
+ );
132
+ }
133
+ if (digest(result.data) !== reference.sha256) {
134
+ builderPrivatePayloadUnavailable(
135
+ `${args.label} private blob failed integrity verification.`,
136
+ );
137
+ }
138
+ try {
139
+ const envelope = JSON.parse(
140
+ textDecoder.decode(result.data),
141
+ ) as BuilderBlobEnvelope<T>;
142
+ if (
143
+ envelope?.kind !== "agent-native.builder-private-payload" ||
144
+ envelope.version !== 1 ||
145
+ !sameBinding(envelope.binding, args.binding)
146
+ ) {
147
+ builderPrivatePayloadUnavailable(
148
+ `${args.label} private blob identity is invalid.`,
149
+ );
150
+ }
151
+ return envelope.payload;
152
+ } catch (error) {
153
+ if (error instanceof ActionContractError) throw error;
154
+ builderPrivatePayloadUnavailable(
155
+ `${args.label} private blob payload is malformed.`,
156
+ error,
157
+ );
158
+ }
159
+ }
160
+
161
+ export async function putBuilderPrivatePayload<T>(args: {
162
+ payload: T;
163
+ binding: BuilderBlobBinding;
164
+ label: string;
165
+ ownerEmail?: string;
166
+ }): Promise<string> {
167
+ const bytes = textEncoder.encode(
168
+ JSON.stringify({
169
+ kind: "agent-native.builder-private-payload",
170
+ version: 1,
171
+ binding: args.binding,
172
+ payload: args.payload,
173
+ } satisfies BuilderBlobEnvelope<T>),
174
+ );
175
+ let handle: PrivateBlobHandle | null;
176
+ try {
177
+ handle = await putPrivateBlob({
178
+ data: bytes,
179
+ key: `content/builder/${createHash("sha256").update(stableJson(args.binding)).digest("hex")}/${randomUUID()}.json`,
180
+ filename: "builder-private-payload.json",
181
+ mimeType: "application/json",
182
+ ownerEmail: args.ownerEmail,
183
+ metadata: { kind: "builder-private-payload", version: 1 },
184
+ });
185
+ } catch (error) {
186
+ builderPrivatePayloadUnavailable(
187
+ `${args.label} could not be stored in private blob custody.`,
188
+ error,
189
+ );
190
+ }
191
+ if (!handle)
192
+ builderPrivatePayloadUnavailable(
193
+ `${args.label} requires configured private blob custody.`,
194
+ );
195
+ const reference = JSON.stringify({
196
+ kind: "agent-native.builder-private-payload",
197
+ version: 1,
198
+ binding: args.binding,
199
+ sha256: digest(bytes),
200
+ handle,
201
+ } satisfies BuilderBlobReference);
202
+ try {
203
+ await readBuilderPrivatePayload({
204
+ reference,
205
+ binding: args.binding,
206
+ label: args.label,
207
+ });
208
+ } catch (error) {
209
+ let cleanupReason: string | null = null;
210
+ try {
211
+ const cleanup = await deletePrivateBlob(handle);
212
+ if (!cleanup.deleted) {
213
+ cleanupReason = cleanup.reason ?? "Deletion was declined";
214
+ }
215
+ } catch (cleanupError) {
216
+ cleanupReason =
217
+ cleanupError instanceof Error ? cleanupError.name : "Unknown error";
218
+ }
219
+ if (cleanupReason) {
220
+ try {
221
+ await enqueueBuilderPrivatePayloadCleanup(getDb(), reference);
222
+ } catch {
223
+ // Keep the provider failure visible if durable tracking is unavailable.
224
+ }
225
+ logBuilderPrivatePayloadCleanupFailure(args.label, handle, cleanupReason);
226
+ }
227
+ throw error;
228
+ }
229
+ return reference;
230
+ }
231
+
232
+ export async function deleteBuilderPrivatePayload(reference: unknown) {
233
+ const parsed = parseReference(reference);
234
+ if (!parsed) return false;
235
+ const result = await deletePrivateBlob(parsed.handle);
236
+ return result.deleted;
237
+ }
238
+
239
+ export async function enqueueBuilderPrivatePayloadCleanup(
240
+ db: ReturnType<typeof getDb>,
241
+ reference: unknown,
242
+ ) {
243
+ if (!isBuilderPrivatePayloadReference(reference)) return;
244
+ await db
245
+ .insert(schema.contentDatabaseBuilderBlobCleanup)
246
+ .values({ blobReference: reference as string })
247
+ .onConflictDoNothing();
248
+ }
249
+
250
+ async function drainBuilderPrivatePayloadCleanupQueue(args: {
251
+ db: ReturnType<typeof getDb>;
252
+ label: string;
253
+ deadlineAt?: number;
254
+ signal?: AbortSignal;
255
+ }) {
256
+ const cleanupQueue = schema.contentDatabaseBuilderBlobCleanup;
257
+
258
+ let pending: Array<{ blobReference: string; attemptCount: number }>;
259
+ try {
260
+ pending = await args.db
261
+ .select({
262
+ blobReference: cleanupQueue.blobReference,
263
+ attemptCount: cleanupQueue.attemptCount,
264
+ })
265
+ .from(cleanupQueue)
266
+ .where(lte(cleanupQueue.nextAttemptAt, new Date().toISOString()))
267
+ .orderBy(asc(cleanupQueue.nextAttemptAt), asc(cleanupQueue.createdAt))
268
+ .limit(BUILDER_PRIVATE_PAYLOAD_CLEANUP_BATCH_SIZE);
269
+ } catch (error) {
270
+ console.error(
271
+ `[builder-private-payload] ${args.label} cleanup queue could not be read`,
272
+ error,
273
+ );
274
+ return;
275
+ }
276
+
277
+ for (const queued of pending) {
278
+ if (
279
+ args.signal?.aborted ||
280
+ (args.deadlineAt !== undefined && Date.now() >= args.deadlineAt)
281
+ ) {
282
+ return;
283
+ }
284
+ try {
285
+ const deleted = await deleteBuilderPrivatePayload(queued.blobReference);
286
+ if (!deleted) {
287
+ console.error(
288
+ `[builder-private-payload] ${args.label} cleanup was declined; retry retained`,
289
+ );
290
+ await scheduleBuilderPrivatePayloadCleanupRetry(args, queued);
291
+ continue;
292
+ }
293
+ await args.db
294
+ .delete(cleanupQueue)
295
+ .where(eq(cleanupQueue.blobReference, queued.blobReference));
296
+ } catch (error) {
297
+ console.error(
298
+ `[builder-private-payload] ${args.label} cleanup remains queued`,
299
+ error,
300
+ );
301
+ await scheduleBuilderPrivatePayloadCleanupRetry(args, queued);
302
+ }
303
+ }
304
+ }
305
+
306
+ async function scheduleBuilderPrivatePayloadCleanupRetry(
307
+ args: {
308
+ db: ReturnType<typeof getDb>;
309
+ label: string;
310
+ },
311
+ queued: { blobReference: string; attemptCount: number },
312
+ ) {
313
+ const attemptCount = queued.attemptCount + 1;
314
+ const delayMs = Math.min(
315
+ BUILDER_PRIVATE_PAYLOAD_CLEANUP_RETRY_BASE_MS *
316
+ 2 ** Math.min(attemptCount - 1, 10),
317
+ BUILDER_PRIVATE_PAYLOAD_CLEANUP_RETRY_MAX_MS,
318
+ );
319
+ try {
320
+ await args.db
321
+ .update(schema.contentDatabaseBuilderBlobCleanup)
322
+ .set({
323
+ attemptCount,
324
+ nextAttemptAt: new Date(Date.now() + delayMs).toISOString(),
325
+ })
326
+ .where(
327
+ eq(
328
+ schema.contentDatabaseBuilderBlobCleanup.blobReference,
329
+ queued.blobReference,
330
+ ),
331
+ );
332
+ } catch (error) {
333
+ console.error(
334
+ `[builder-private-payload] ${args.label} cleanup retry could not be scheduled`,
335
+ error,
336
+ );
337
+ }
338
+ }
339
+
340
+ export async function drainBuilderPrivatePayloadCleanup(
341
+ args: {
342
+ deadlineAt?: number;
343
+ signal?: AbortSignal;
344
+ } = {},
345
+ ) {
346
+ await drainBuilderPrivatePayloadCleanupQueue({
347
+ db: getDb(),
348
+ label: "scheduled",
349
+ deadlineAt: args.deadlineAt,
350
+ signal: args.signal,
351
+ });
352
+ }
353
+
354
+ export async function cleanupBuilderPrivatePayload(
355
+ reference: unknown,
356
+ _label: string,
357
+ ) {
358
+ if (!isBuilderPrivatePayloadReference(reference)) return;
359
+ const blobReference = reference as string;
360
+ await enqueueBuilderPrivatePayloadCleanup(getDb(), blobReference);
361
+ }
362
+
363
+ export function isBuilderPrivatePayloadReference(value: unknown) {
364
+ return parseReference(value) !== null;
365
+ }
366
+
367
+ export function isBuilderPrivatePayloadBoundToSource(
368
+ value: unknown,
369
+ ownerEmail: string,
370
+ sourceId: string,
371
+ ) {
372
+ const reference = parseReference(value);
373
+ return (
374
+ reference?.binding.ownerEmail === ownerEmail &&
375
+ reference.binding.sourceId === sourceId
376
+ );
377
+ }
378
+
379
+ export function builderExecutionPayloadReference(
380
+ payloadJson: string,
381
+ ): string | null {
382
+ try {
383
+ const payload = JSON.parse(payloadJson) as Record<string, unknown>;
384
+ const reference = payload?.[BUILDER_EXECUTION_PAYLOAD_BLOB_KEY];
385
+ return isBuilderPrivatePayloadReference(reference)
386
+ ? (reference as string)
387
+ : null;
388
+ // coercion-ok: malformed compact SQL cannot authorize execution-blob deletion.
389
+ } catch {
390
+ return null;
391
+ }
392
+ }
393
+
394
+ export interface BuilderExecutionPayloadBinding {
395
+ ownerEmail: string;
396
+ sourceId: string;
397
+ changeSetId: string;
398
+ executionId: string;
399
+ idempotencyKey: string;
400
+ }
401
+
402
+ function executionBinding(
403
+ value: BuilderExecutionPayloadBinding,
404
+ ): BuilderBlobBinding {
405
+ return { ...value };
406
+ }
407
+
408
+ export async function storeBuilderExecutionPayload(args: {
409
+ payload: unknown;
410
+ binding: BuilderExecutionPayloadBinding;
411
+ }) {
412
+ if (
413
+ !args.payload ||
414
+ typeof args.payload !== "object" ||
415
+ Array.isArray(args.payload)
416
+ ) {
417
+ builderPrivatePayloadUnavailable("Builder execution payload is malformed.");
418
+ }
419
+ const payload = args.payload as Record<string, unknown>;
420
+ const reference = await putBuilderPrivatePayload({
421
+ payload,
422
+ binding: executionBinding(args.binding),
423
+ label: "Builder prepared request and receipt",
424
+ ownerEmail: args.binding.ownerEmail,
425
+ });
426
+ const response =
427
+ payload.response && typeof payload.response === "object"
428
+ ? { stored: true }
429
+ : undefined;
430
+ return JSON.stringify({
431
+ [BUILDER_EXECUTION_PAYLOAD_BLOB_KEY]: reference,
432
+ ...(response ? { response } : {}),
433
+ });
434
+ }
435
+
436
+ export async function readBuilderExecutionPayload(args: {
437
+ payloadJson: string;
438
+ binding: BuilderExecutionPayloadBinding;
439
+ }): Promise<Record<string, unknown>> {
440
+ let stored: Record<string, unknown>;
441
+ try {
442
+ const parsed = JSON.parse(args.payloadJson) as unknown;
443
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
444
+ builderPrivatePayloadUnavailable(
445
+ "Builder execution payload JSON is malformed.",
446
+ );
447
+ }
448
+ stored = parsed as Record<string, unknown>;
449
+ } catch {
450
+ builderPrivatePayloadUnavailable(
451
+ "Builder execution payload JSON is malformed.",
452
+ );
453
+ }
454
+ const reference = stored[BUILDER_EXECUTION_PAYLOAD_BLOB_KEY];
455
+ if (reference === undefined) return stored;
456
+ const payload = await readBuilderPrivatePayload<unknown>({
457
+ reference,
458
+ binding: executionBinding(args.binding),
459
+ label: "Builder prepared request and receipt",
460
+ });
461
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
462
+ builderPrivatePayloadUnavailable(
463
+ "Builder execution private blob payload is malformed.",
464
+ );
465
+ }
466
+ return payload as Record<string, unknown>;
467
+ }
@@ -0,0 +1,17 @@
1
+ export const BUILDER_EXECUTION_CLAIM_TIMEOUT_MS = 10 * 60 * 1000;
2
+
3
+ export function builderExecutionStaleBefore(now: string) {
4
+ const timestamp = Date.parse(now);
5
+ return new Date(
6
+ (Number.isFinite(timestamp) ? timestamp : Date.now()) -
7
+ BUILDER_EXECUTION_CLAIM_TIMEOUT_MS,
8
+ ).toISOString();
9
+ }
10
+
11
+ export function isBuilderExecutionClaimStale(updatedAt: string, now: string) {
12
+ const updatedTimestamp = Date.parse(updatedAt);
13
+ return (
14
+ Number.isFinite(updatedTimestamp) &&
15
+ updatedTimestamp < Date.parse(builderExecutionStaleBefore(now))
16
+ );
17
+ }
@@ -67,6 +67,8 @@ export async function lookupBuilderCmsSafeModelIntent(args: {
67
67
  marker?: string;
68
68
  exactTitle?: string;
69
69
  intendedFields?: Record<string, unknown>;
70
+ expectedSourceSpace?: string | null;
71
+ expectedSourceConnectionId?: string | null;
70
72
  }) {
71
73
  if (!args.marker && !args.exactTitle) {
72
74
  throw new Error("Provide an exact execution marker or exact title.");
@@ -75,7 +77,10 @@ export async function lookupBuilderCmsSafeModelIntent(args: {
75
77
  model: BUILDER_CMS_SAFE_WRITE_MODEL,
76
78
  rawData: true,
77
79
  requirePrivateKey: true,
80
+ requireBoundOAuth: true,
78
81
  limit: 10_000,
82
+ expectedSourceSpace: args.expectedSourceSpace,
83
+ expectedSourceConnectionId: args.expectedSourceConnectionId,
79
84
  });
80
85
  if (result.state !== "live" || result.progress.partial) {
81
86
  throw new Error(