@agent-native/core-corpus 0.216.0 → 0.217.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 (149) 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 +1 -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/i18n/en-US.ts +27 -0
  11. package/corpus/templates/analytics/app/pages/sessions/EventCatalogPage.tsx +18 -1
  12. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +3 -1
  13. package/corpus/templates/analytics/app/pages/sessions/SessionOutputs.tsx +197 -0
  14. package/corpus/templates/analytics/app/pages/sessions/SessionsTriagePage.tsx +114 -4
  15. package/corpus/templates/analytics/changelog/2026-10-15-sessions-can-find-people-who-made-a-slides-deck-but-didn-t-p.md +6 -0
  16. package/corpus/templates/analytics/changelog/2026-10-16-the-activation-funnel-counts-visitors-who-decline-analytics-.md +6 -0
  17. package/corpus/templates/analytics/server/db/schema.ts +67 -0
  18. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +9 -0
  19. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +28 -22
  20. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +36 -1
  21. package/corpus/templates/analytics/server/lib/output-outcomes.ts +712 -0
  22. package/corpus/templates/analytics/server/lib/replay-origins.ts +33 -0
  23. package/corpus/templates/analytics/server/lib/session-event-index.ts +20 -0
  24. package/corpus/templates/analytics/server/lib/session-performance.ts +6 -40
  25. package/corpus/templates/analytics/server/lib/session-recording-associations.ts +30 -1
  26. package/corpus/templates/analytics/server/lib/session-replay-triage.ts +99 -0
  27. package/corpus/templates/analytics/server/lib/session-replay.ts +45 -31
  28. package/corpus/templates/analytics/server/lib/sessions-triage-lab.ts +8 -2
  29. package/corpus/templates/analytics/server/plugins/db.ts +46 -0
  30. package/corpus/templates/analytics/shared/output-use.ts +205 -0
  31. package/corpus/templates/analytics/shared/session-events.ts +6 -0
  32. package/corpus/templates/calendar/CHANGELOG.md +6 -0
  33. package/corpus/templates/calendar/actions/create-event.ts +2 -4
  34. package/corpus/templates/calendar/actions/update-event.ts +35 -6
  35. package/corpus/templates/calendar/changelog/2026-10-21-solo-events-avoid-notification-approval.md +6 -0
  36. package/corpus/templates/community/qr/package.json +1 -1
  37. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -3
  38. package/corpus/templates/content/CHANGELOG.md +8 -0
  39. package/corpus/templates/content/actions/_browser-document-save-attempt.ts +23 -1
  40. package/corpus/templates/content/actions/_builder-cms-blob-custody.ts +467 -0
  41. package/corpus/templates/content/actions/_builder-cms-execution-lease.ts +17 -0
  42. package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +5 -0
  43. package/corpus/templates/content/actions/_builder-cms-read-client.ts +881 -21
  44. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +28 -2
  45. package/corpus/templates/content/actions/_builder-cms-write-client.ts +109 -11
  46. package/corpus/templates/content/actions/_builder-docs-client.ts +1 -0
  47. package/corpus/templates/content/actions/_builder-private-blob.test-fixture.ts +90 -0
  48. package/corpus/templates/content/actions/_builder-source-execution-guard.ts +48 -0
  49. package/corpus/templates/content/actions/_content-save-outcomes.ts +5 -0
  50. package/corpus/templates/content/actions/_database-source-utils.ts +290 -8
  51. package/corpus/templates/content/actions/_database-utils.ts +93 -7
  52. package/corpus/templates/content/actions/_database-view-setup.ts +21 -2
  53. package/corpus/templates/content/actions/_delete-content-space.ts +8 -1
  54. package/corpus/templates/content/actions/_github-source-freshness.ts +30 -4
  55. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -6
  56. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +110 -26
  57. package/corpus/templates/content/actions/change-content-database-source-role.ts +51 -7
  58. package/corpus/templates/content/actions/delete-document.ts +41 -3
  59. package/corpus/templates/content/actions/disconnect-content-database-source.ts +68 -2
  60. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +33 -2
  61. package/corpus/templates/content/actions/execute-builder-source-execution.ts +900 -140
  62. package/corpus/templates/content/actions/get-document.ts +2 -0
  63. package/corpus/templates/content/actions/permanently-delete-document.ts +6 -0
  64. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +155 -13
  65. package/corpus/templates/content/actions/prepare-builder-source-review.ts +299 -73
  66. package/corpus/templates/content/actions/set-live-body-switch.ts +45 -0
  67. package/corpus/templates/content/actions/update-document.ts +139 -20
  68. package/corpus/templates/content/actions/validate-builder-source-execution.ts +135 -26
  69. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +23 -0
  70. package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +27 -2
  71. package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +32 -8
  72. package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +27 -2
  73. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +1 -0
  74. package/corpus/templates/content/app/hooks/use-content-database.ts +40 -2
  75. package/corpus/templates/content/app/hooks/use-documents.ts +14 -0
  76. package/corpus/templates/content/app/i18n-data.ts +1 -0
  77. package/corpus/templates/content/changelog/2026-10-17-builder-content-avoids-retrying-a-write-when-builder-accepte.md +6 -0
  78. package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +1 -1
  79. package/corpus/templates/content/changelog/2026-10-20-builder-content-sync-keeps-read-and-write-status-accurate-wh.md +6 -0
  80. package/corpus/templates/content/changelog/2026-10-21-list-rows-can-show-each-pages-first-lines.md +6 -0
  81. package/corpus/templates/content/changelog/2026-10-22-a-collection-linked-to-a-big-github-folder-fills-faster-and-p.md +6 -0
  82. package/corpus/templates/content/docs/product/capabilities/content.source.adapters.md +29 -0
  83. package/corpus/templates/content/parity/matrix.md +42 -42
  84. package/corpus/templates/content/parity/matrix.ts +3 -1
  85. package/corpus/templates/content/server/db/schema.ts +20 -0
  86. package/corpus/templates/content/server/lib/content-trash-purge.ts +15 -1
  87. package/corpus/templates/content/server/lib/live-body-writer.ts +138 -26
  88. package/corpus/templates/content/server/live-copy/binding.ts +31 -0
  89. package/corpus/templates/content/server/live-copy/bounded-task.d.ts +6 -0
  90. package/corpus/templates/content/server/live-copy/exec-db.ts +1 -0
  91. package/corpus/templates/content/server/live-copy/holds.ts +59 -0
  92. package/corpus/templates/content/server/live-copy/persist.ts +656 -0
  93. package/corpus/templates/content/server/live-copy/switch.ts +136 -0
  94. package/corpus/templates/content/server/live-copy/task-names.ts +4 -0
  95. package/corpus/templates/content/server/live-copy/tasks.ts +183 -0
  96. package/corpus/templates/content/server/live-copy/write-cause.ts +85 -0
  97. package/corpus/templates/content/server/live-copy/write.ts +85 -0
  98. package/corpus/templates/content/server/plugins/builder-private-payload-cleanup.ts +22 -0
  99. package/corpus/templates/content/server/plugins/collab.ts +3 -0
  100. package/corpus/templates/content/server/plugins/db.ts +28 -0
  101. package/corpus/templates/content/shared/api.ts +30 -1
  102. package/corpus/templates/content/shared/database-item-layout.ts +54 -22
  103. package/corpus/templates/content/shared/document-intent-merge.ts +16 -9
  104. package/corpus/templates/content/shared/og-description.ts +258 -36
  105. package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +5 -1
  106. package/corpus/templates/design/.generated/bridge/shader-runtime.generated.ts +25 -4
  107. package/corpus/templates/design/CHANGELOG.md +2 -0
  108. package/corpus/templates/design/actions/get-getting-started.ts +53 -0
  109. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +10 -0
  110. package/corpus/templates/design/app/components/design/bridge/native-shader-runtime.bridge.ts +132 -79
  111. package/corpus/templates/design/app/components/design/bridge/native-source-provider.ts +268 -130
  112. package/corpus/templates/design/app/components/design/bridge/native-worker-scene-host.ts +122 -42
  113. package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +41 -4
  114. package/corpus/templates/design/app/components/design/design-canvas/text-content-change-result.ts +46 -12
  115. package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +15 -8
  116. package/corpus/templates/design/app/components/layout/GettingStarted.tsx +50 -0
  117. package/corpus/templates/design/app/components/layout/Sidebar.tsx +3 -0
  118. package/corpus/templates/design/app/components/templates/DesignTemplateGrid.tsx +163 -0
  119. package/corpus/templates/design/app/components/templates/DesignTemplateLibrary.tsx +65 -78
  120. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +6 -2
  121. package/corpus/templates/design/app/global.css +6 -0
  122. package/corpus/templates/design/app/i18n/en-US.ts +17 -0
  123. package/corpus/templates/design/app/lib/canvas-load-outcome.ts +147 -0
  124. package/corpus/templates/design/app/lib/canvas-outcome-tracking.ts +531 -0
  125. package/corpus/templates/design/app/lib/canvas-style-kind.ts +50 -0
  126. package/corpus/templates/design/app/pages/Index.tsx +10 -3
  127. package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +86 -29
  128. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +38 -3
  129. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-core.ts +9 -0
  130. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-files-and-saving.ts +2 -0
  131. package/corpus/templates/design/changelog/2026-10-21-a-getting-started-checklist-at-the-bottom-of-the-sidebar-tra.md +6 -0
  132. package/corpus/templates/design/changelog/2026-10-21-templates-now-open-a-read-only-preview-from-the-card-and-the.md +6 -0
  133. package/corpus/templates/design/changelog/2026-10-22-collapsed-shader-layers-no-longer-interrupt-other-shaders-on.md +6 -0
  134. package/corpus/templates/design/changelog/2026-10-22-shader-fills-keep-rendering-when-other-screen-content-cannot.md +6 -0
  135. package/corpus/templates/design/scripts/design-export-validation/export-baseline.json +24 -0
  136. package/corpus/templates/design/server/lib/getting-started.ts +267 -0
  137. package/corpus/templates/design/server/plugins/getting-started.ts +5 -0
  138. package/corpus/templates/design/shared/design-template-presets/analytics-dashboard.ts +127 -0
  139. package/corpus/templates/design/shared/design-template-presets/design-studio.ts +98 -0
  140. package/corpus/templates/design/shared/design-template-presets/messaging-app.ts +101 -0
  141. package/corpus/templates/design/shared/design-template-presets/project-tasks.ts +120 -0
  142. package/corpus/templates/design/shared/design-template-presets.ts +8 -0
  143. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
  144. package/corpus/templates/slides/CHANGELOG.md +6 -0
  145. package/corpus/templates/slides/actions/delete-deck.ts +7 -0
  146. package/corpus/templates/slides/changelog/2026-10-21-connected-deck-deletion-requires-approval.md +6 -0
  147. package/corpus/templates/slides/scripts/edit-fidelity/client-action.ts +61 -0
  148. package/corpus/templates/slides/scripts/edit-fidelity/run.ts +10 -0
  149. package/package.json +1 -1
@@ -36,6 +36,8 @@ export interface BuilderCmsExecutionPayload {
36
36
  sourceQualifiedId: string | null;
37
37
  documentId: string | null;
38
38
  databaseItemId: string | null;
39
+ /** The entry's lastUpdated when the change was reviewed; execute rejects a live value that differs. */
40
+ reviewedLastUpdated: string | null;
39
41
  };
40
42
  request: {
41
43
  method: "POST" | "PATCH";
@@ -872,6 +874,14 @@ export function buildBuilderCmsExecutionPlan(args: {
872
874
  targetRow,
873
875
  request,
874
876
  });
877
+ const reviewedLastUpdated = targetEntryId
878
+ ? (targetRow?.lastSourceUpdatedAt ?? null)
879
+ : null;
880
+ if (targetEntryId && !reviewedLastUpdated?.trim()) {
881
+ fieldBlockers.push(
882
+ "Refresh this Builder entry before writing so its last update time can be checked.",
883
+ );
884
+ }
875
885
  const safety = builderSafetyChecks({
876
886
  source: args.source,
877
887
  changeSet: args.changeSet,
@@ -934,6 +944,7 @@ export function buildBuilderCmsExecutionPlan(args: {
934
944
  sourceQualifiedId: targetSourceQualifiedId,
935
945
  documentId: args.changeSet.documentId,
936
946
  databaseItemId: args.changeSet.databaseItemId,
947
+ reviewedLastUpdated,
937
948
  },
938
949
  pushMode: effectivePushMode,
939
950
  request,
@@ -1003,11 +1014,26 @@ export function validateBuilderCmsExecutionDryRun(args: {
1003
1014
  "Stored Builder effect no longer matches the approved write mode.",
1004
1015
  );
1005
1016
  }
1017
+ const { reviewedLastUpdated: storedReviewedLastUpdated, ...storedTarget } =
1018
+ (storedComparable.target ?? {}) as Partial<
1019
+ BuilderCmsExecutionPayload["target"]
1020
+ >;
1021
+ const { reviewedLastUpdated: planReviewedLastUpdated, ...planTarget } =
1022
+ (planComparable.target ?? {}) as Partial<
1023
+ BuilderCmsExecutionPayload["target"]
1024
+ >;
1025
+ if (stableJson(storedTarget) !== stableJson(planTarget)) {
1026
+ mismatches.push(
1027
+ "Stored Builder target no longer matches the current row identity.",
1028
+ );
1029
+ }
1030
+ // A refresh that saw a newer Builder lastUpdated moves the row baseline, so
1031
+ // the value recorded at review is the one the live check must compare.
1006
1032
  if (
1007
- stableJson(storedComparable.target) !== stableJson(planComparable.target)
1033
+ (storedReviewedLastUpdated ?? null) !== (planReviewedLastUpdated ?? null)
1008
1034
  ) {
1009
1035
  mismatches.push(
1010
- "Stored Builder target no longer matches the current row identity.",
1036
+ "Builder entry changed since this change was reviewed. Refresh and review it again.",
1011
1037
  );
1012
1038
  }
1013
1039
 
@@ -1,4 +1,11 @@
1
- import { resolveBuilderCredential } from "@agent-native/core/server";
1
+ import { fail, isActionContractError } from "@agent-native/core/action";
2
+ import {
3
+ BUILDER_CONTENT_WRITE_SCOPE,
4
+ BUILDER_OAUTH_RESOURCE,
5
+ resolveBuilderLegacyRequestAuthorization,
6
+ resolveBuilderRequestAuthorization,
7
+ type BuilderRequestAuthorization,
8
+ } from "@agent-native/core/server";
2
9
 
3
10
  export interface BuilderCmsWriteRequest {
4
11
  method: "POST" | "PATCH";
@@ -20,7 +27,10 @@ export const DEFAULT_BUILDER_CMS_WRITE_TIMEOUT_MS = 30_000;
20
27
 
21
28
  type FetchLike = typeof fetch;
22
29
 
23
- function builderWriteApiHost() {
30
+ function builderWriteApiHost(source: BuilderRequestAuthorization["source"]) {
31
+ // An OAuth bearer goes only to the Builder API resource it was issued for;
32
+ // the host overrides apply to private keys alone.
33
+ if (source === "oauth") return BUILDER_OAUTH_RESOURCE;
24
34
  return (
25
35
  process.env.BUILDER_CONTENT_API_HOST ??
26
36
  process.env.BUILDER_CMS_API_HOST ??
@@ -28,11 +38,80 @@ function builderWriteApiHost() {
28
38
  ).replace(/\/+$/, "");
29
39
  }
30
40
 
31
- async function readBuilderPrivateKey() {
32
- return (
33
- (await resolveBuilderCredential("BUILDER_PRIVATE_KEY")) ??
34
- (await resolveBuilderCredential("BUILDER_CMS_PRIVATE_KEY"))
41
+ const BUILDER_WRITE_LEGACY_CREDENTIAL_KEYS = [
42
+ "BUILDER_PRIVATE_KEY",
43
+ "BUILDER_CMS_PRIVATE_KEY",
44
+ ] as const;
45
+
46
+ async function readBuilderWriteAuthorization(sourceBound: boolean) {
47
+ try {
48
+ return await resolveBuilderRequestAuthorization({
49
+ oauthResource: "general",
50
+ requiredScope: BUILDER_CONTENT_WRITE_SCOPE,
51
+ legacyCredentialKeys: BUILDER_WRITE_LEGACY_CREDENTIAL_KEYS,
52
+ });
53
+ } catch (error) {
54
+ // The shared resolver throws instead of falling back when a grant predates
55
+ // builder:content:write. A caller with no Source binding wrote with the
56
+ // deploy keys before OAuth, so it keeps that path until re-authorizing.
57
+ if (
58
+ sourceBound ||
59
+ !isActionContractError(error) ||
60
+ error.errorCode !== "builder_oauth_reauthorization_required" ||
61
+ error.details?.reason !== "missing_scope" ||
62
+ error.details.scope !== BUILDER_CONTENT_WRITE_SCOPE
63
+ ) {
64
+ throw error;
65
+ }
66
+ const legacy = await resolveBuilderLegacyRequestAuthorization(
67
+ BUILDER_WRITE_LEGACY_CREDENTIAL_KEYS,
68
+ );
69
+ if (!legacy) throw error;
70
+ return legacy;
71
+ }
72
+ }
73
+
74
+ function assertBuilderWriteSourceBinding(
75
+ authorization: Awaited<ReturnType<typeof readBuilderWriteAuthorization>>,
76
+ expectedSourceSpace: string | null | undefined,
77
+ expectedSourceConnectionId: string | null | undefined,
78
+ sourceWrite: boolean,
79
+ ) {
80
+ const sourceBound = Boolean(
81
+ expectedSourceSpace || expectedSourceConnectionId,
35
82
  );
83
+ // An unbound Source keeps writing with its deploy keys, but OAuth may write
84
+ // only into the space a Source is bound to.
85
+ if (
86
+ sourceBound &&
87
+ (authorization?.source !== "oauth" ||
88
+ authorization.oauthResource !== "general")
89
+ ) {
90
+ fail(
91
+ "This Builder source's OAuth connection is unavailable. Reconnect the source before writing.",
92
+ { errorCode: "builder_connection_required", statusCode: 412 },
93
+ );
94
+ }
95
+ if (authorization?.source !== "oauth") return;
96
+ if (!sourceWrite && !sourceBound) return;
97
+ if (!expectedSourceSpace || !expectedSourceConnectionId) {
98
+ fail(
99
+ "This Builder source is not bound to its connected space. Refresh the source before writing.",
100
+ { errorCode: "builder_source_unbound", statusCode: 409 },
101
+ );
102
+ }
103
+ if (authorization.oauthSelectedPublicKey !== expectedSourceSpace) {
104
+ fail(
105
+ "The connected Builder space does not match this Content source. Reconnect the source's Builder space before writing.",
106
+ { errorCode: "builder_source_space_mismatch", statusCode: 409 },
107
+ );
108
+ }
109
+ if (authorization.oauthConnectionId !== expectedSourceConnectionId) {
110
+ fail(
111
+ "The connected Builder credential does not match this Content source. Reconnect the source before writing.",
112
+ { errorCode: "builder_source_connection_mismatch", statusCode: 409 },
113
+ );
114
+ }
36
115
  }
37
116
 
38
117
  function parseResponseBody(text: string): unknown {
@@ -136,22 +215,41 @@ function buildWriteResult(args: {
136
215
 
137
216
  export async function executeBuilderCmsWrite(args: {
138
217
  request: BuilderCmsWriteRequest;
218
+ expectedSourceSpace?: string | null;
219
+ expectedSourceConnectionId?: string | null;
220
+ requireSourceBinding?: boolean;
221
+ legacyOnly?: boolean;
139
222
  fetchImpl?: FetchLike;
140
223
  /** @deprecated Never used: retrying another transport after dispatch is unsafe. */
141
224
  nodeRequestImpl?: unknown;
142
225
  timeoutMs?: number;
143
226
  }): Promise<BuilderCmsWriteResult> {
144
- const privateKey = await readBuilderPrivateKey();
145
- if (!privateKey) {
227
+ const authorization = args.legacyOnly
228
+ ? await resolveBuilderLegacyRequestAuthorization(
229
+ BUILDER_WRITE_LEGACY_CREDENTIAL_KEYS,
230
+ )
231
+ : await readBuilderWriteAuthorization(
232
+ Boolean(args.expectedSourceSpace || args.expectedSourceConnectionId),
233
+ );
234
+ if (!authorization) {
146
235
  return {
147
236
  ok: false,
148
237
  status: 0,
149
238
  responseBody: null,
150
- error: "Builder private key is not configured.",
239
+ error: "Builder write access is not connected.",
151
240
  };
152
241
  }
242
+ assertBuilderWriteSourceBinding(
243
+ authorization,
244
+ args.expectedSourceSpace,
245
+ args.expectedSourceConnectionId,
246
+ args.requireSourceBinding === true,
247
+ );
153
248
 
154
- const url = new URL(args.request.path, builderWriteApiHost());
249
+ const url = new URL(
250
+ args.request.path,
251
+ builderWriteApiHost(authorization.source),
252
+ );
155
253
  for (const [key, value] of Object.entries(args.request.query ?? {})) {
156
254
  url.searchParams.set(key, value);
157
255
  }
@@ -159,7 +257,7 @@ export async function executeBuilderCmsWrite(args: {
159
257
  const body = JSON.stringify(args.request.body);
160
258
  const headers = {
161
259
  accept: "application/json",
162
- authorization: `Bearer ${privateKey}`,
260
+ authorization: authorization.authorization,
163
261
  "content-type": "application/json",
164
262
  };
165
263
 
@@ -1105,6 +1105,7 @@ export async function pushBuilderDocsSource(args: {
1105
1105
 
1106
1106
  const writeResult = await executeBuilderCmsWrite({
1107
1107
  request,
1108
+ legacyOnly: true,
1108
1109
  fetchImpl: args.fetchImpl,
1109
1110
  });
1110
1111
  const refreshedDocument =
@@ -0,0 +1,90 @@
1
+ import {
2
+ PrivateBlobError,
3
+ registerPrivateBlobProvider,
4
+ unregisterPrivateBlobProvider,
5
+ type PrivateBlobHandle,
6
+ type PrivateBlobProvider,
7
+ } from "@agent-native/core/private-blob";
8
+ import { expect } from "vitest";
9
+
10
+ import {
11
+ BUILDER_EXECUTION_PAYLOAD_BLOB_KEY,
12
+ readBuilderExecutionPayload,
13
+ } from "./_builder-cms-blob-custody";
14
+
15
+ const PROVIDER_ID = "content-test-memory";
16
+
17
+ export function registerMemoryPrivateBlobProvider() {
18
+ const blobs = new Map<string, Uint8Array>();
19
+ let nextId = 0;
20
+ let putsUnavailable = false;
21
+ let deletesUnavailable = false;
22
+ let deleteCalls = 0;
23
+ const provider: PrivateBlobProvider = {
24
+ id: PROVIDER_ID,
25
+ name: "Content test memory",
26
+ isConfigured: () => true,
27
+ put: async (input) => {
28
+ if (putsUnavailable) {
29
+ throw new PrivateBlobError("store unavailable", "unavailable");
30
+ }
31
+ const id = `${PROVIDER_ID}:${++nextId}`;
32
+ blobs.set(id, new Uint8Array(input.data));
33
+ return { id, provider: PROVIDER_ID, opaque: true, encrypted: false };
34
+ },
35
+ read: async (handle: PrivateBlobHandle) => {
36
+ const data = blobs.get(handle.id);
37
+ if (!data) throw new PrivateBlobError("missing", "not_found");
38
+ return { data, handle };
39
+ },
40
+ delete: async (handle: PrivateBlobHandle) => {
41
+ deleteCalls += 1;
42
+ return {
43
+ ...(deletesUnavailable
44
+ ? { deleted: false, reason: "temporary storage failure" }
45
+ : { deleted: blobs.delete(handle.id) }),
46
+ provider: PROVIDER_ID,
47
+ };
48
+ },
49
+ };
50
+ registerPrivateBlobProvider(provider);
51
+ return {
52
+ blobs,
53
+ setPutsUnavailable: (value: boolean) => {
54
+ putsUnavailable = value;
55
+ },
56
+ setDeletesUnavailable: (value: boolean) => {
57
+ deletesUnavailable = value;
58
+ },
59
+ getDeleteCalls: () => deleteCalls,
60
+ unregister: () => unregisterPrivateBlobProvider(PROVIDER_ID),
61
+ };
62
+ }
63
+
64
+ export async function storedExecutionPayload(execution: {
65
+ id: string;
66
+ ownerEmail: string;
67
+ sourceId: string;
68
+ changeSetId: string;
69
+ idempotencyKey: string;
70
+ payloadJson: string;
71
+ }): Promise<any> {
72
+ // The reader also accepts legacy inline rows, so assert custody here: SQL
73
+ // holds only the blob reference and a receipt marker, never the request.
74
+ const stored = JSON.parse(execution.payloadJson) as Record<string, unknown>;
75
+ const { [BUILDER_EXECUTION_PAYLOAD_BLOB_KEY]: reference, ...rest } = stored;
76
+ expect(reference).toBeDefined();
77
+ expect(rest).toEqual(
78
+ "response" in rest ? { response: { stored: true } } : {},
79
+ );
80
+ return readBuilderExecutionPayload({
81
+ payloadJson: execution.payloadJson,
82
+ binding: {
83
+ ownerEmail: execution.ownerEmail,
84
+ sourceId: execution.sourceId,
85
+ changeSetId: execution.changeSetId,
86
+ executionId: execution.id,
87
+ idempotencyKey: execution.idempotencyKey,
88
+ },
89
+ });
90
+ }
@@ -0,0 +1,48 @@
1
+ import { fail } from "@agent-native/core/action";
2
+ import { and, inArray } from "drizzle-orm";
3
+
4
+ import { getDb, schema } from "../server/db/index.js";
5
+ import { isBuilderExecutionClaimStale } from "./_builder-cms-execution-lease.js";
6
+
7
+ export async function assertNoActiveBuilderSourceExecutions(
8
+ tx: ReturnType<typeof getDb>,
9
+ sourceIds: string[],
10
+ changeSetIds?: string[],
11
+ ) {
12
+ if (sourceIds.length === 0 || changeSetIds?.length === 0) return;
13
+
14
+ const candidates = await tx
15
+ .select({
16
+ state: schema.contentDatabaseSourceExecutions.state,
17
+ updatedAt: schema.contentDatabaseSourceExecutions.updatedAt,
18
+ })
19
+ .from(schema.contentDatabaseSourceExecutions)
20
+ .where(
21
+ and(
22
+ inArray(schema.contentDatabaseSourceExecutions.sourceId, sourceIds),
23
+ changeSetIds
24
+ ? inArray(
25
+ schema.contentDatabaseSourceExecutions.changeSetId,
26
+ changeSetIds,
27
+ )
28
+ : undefined,
29
+ inArray(schema.contentDatabaseSourceExecutions.state, [
30
+ "running",
31
+ "response_received",
32
+ "reconciliation_required",
33
+ ]),
34
+ ),
35
+ );
36
+ const now = new Date().toISOString();
37
+ const active = candidates.some(
38
+ (execution) =>
39
+ execution.state !== "running" ||
40
+ !isBuilderExecutionClaimStale(execution.updatedAt, now),
41
+ );
42
+ if (active) {
43
+ fail(
44
+ "Cannot replace source records while a Builder execution is running or awaiting reconciliation.",
45
+ { errorCode: "builder_execution_running", statusCode: 409 },
46
+ );
47
+ }
48
+ }
@@ -13,6 +13,7 @@ export type ContentSaveOutcome =
13
13
  | "preserved_to_history"
14
14
  | "merged"
15
15
  | "merged_with_displaced_text"
16
+ | "live_copy_body"
16
17
  | "refused"
17
18
  | "applied"
18
19
  | "displaced_preserved"
@@ -63,6 +64,10 @@ const reasons = new Set([
63
64
  "stale_empty_body",
64
65
  "stale_builder_image",
65
66
  "recovery_base_changed",
67
+ "live_copy_save_held",
68
+ "live_copy_save_unheld",
69
+ "live_copy_save_noncanonical",
70
+ "live_copy_save_unchecked",
66
71
  "title_base_changed",
67
72
  "body_base_changed",
68
73
  "body_revision_cas_conflict",