@agent-native/core-corpus 0.222.1 → 0.222.2-nightly-20261029152548

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 (36) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  3. package/corpus/templates/assets/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  4. package/corpus/templates/brain/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  5. package/corpus/templates/brain/server/plugins/auth.ts +1 -1
  6. package/corpus/templates/brain/server/routes/api/brain/queue/run.post.ts +1 -7
  7. package/corpus/templates/calendar/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  8. package/corpus/templates/chat/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  9. package/corpus/templates/clips/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  10. package/corpus/templates/clips/app/components/clips-command-menu.tsx +12 -7
  11. package/corpus/templates/clips/app/components/import-menu.tsx +4 -1
  12. package/corpus/templates/clips/app/components/library/library-grid.tsx +15 -8
  13. package/corpus/templates/clips/app/components/library/library-layout.tsx +10 -2
  14. package/corpus/templates/clips/app/components/player/share-dialog.tsx +4 -7
  15. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +1 -1
  16. package/corpus/templates/clips/app/routes/_app.screenshots.tsx +16 -0
  17. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +12 -1
  18. package/corpus/templates/clips/changelog/2026-10-28-fixed-the-share-advanced-popover-so-the-save-buttons-fit-and.md +6 -0
  19. package/corpus/templates/clips/changelog/2026-10-29-screenshots-are-available-when-enabled-in-labs.md +6 -0
  20. package/corpus/templates/clips/shared/labs.ts +9 -0
  21. package/corpus/templates/content/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  22. package/corpus/templates/content/actions/execute-builder-source-execution.ts +91 -4
  23. package/corpus/templates/content/changelog/2026-10-27-two-collections-connected-to-the-same-builder-model-can-no-l.md +6 -0
  24. package/corpus/templates/crm/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  25. package/corpus/templates/design/.agents/skills/hubspot-email/SKILL.md +2 -2
  26. package/corpus/templates/design/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  27. package/corpus/templates/design/.generated/bridge/native-builtin-metadata.generated.ts +326 -0
  28. package/corpus/templates/design/.generated/bridge/native-worker-source.generated.ts +6 -0
  29. package/corpus/templates/design/actions/generate-design.ts +19 -16
  30. package/corpus/templates/dispatch/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  31. package/corpus/templates/forms/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  32. package/corpus/templates/mail/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  33. package/corpus/templates/plan/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  34. package/corpus/templates/slides/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  35. package/corpus/templates/slides-2/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  36. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -32,4 +32,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core-corpus/corpu
32
32
 
33
33
  ## Generated Counts
34
34
 
35
- - template files: 15073
35
+ - template files: 15129
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","analytics-alert-jobs","analytics-rollup-backfill-jobs","auth","collab","config","content-metrics-collection-jobs","core-routes","creative-context","dashboard-report-jobs","db","feature-flags","labs","onboarding","otel","session-replay-retention-jobs","transactional-emails","uptime-monitor-jobs"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","auth","creative-context","db","labs","onboarding","otel"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","auth","brain-jobs","core-routes","db","otel"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }
@@ -13,5 +13,5 @@ export default createToolkitAuthPlugin({
13
13
  "Review company-wide knowledge through proposal workflows",
14
14
  ],
15
15
  },
16
- publicPaths: ["/api/_agent-native/brain/ingest"],
16
+ publicPaths: ["/api/_agent-native/brain/ingest", "/api/brain/queue/run"],
17
17
  });
@@ -13,12 +13,6 @@ declare global {
13
13
  export const BRAIN_QUEUE_SWEEP_LIMIT = 25;
14
14
  export const BRAIN_MAX_DISTILLATIONS_PER_SWEEP = 2;
15
15
 
16
- function productionLike() {
17
- return (
18
- process.env.NODE_ENV === "production" || process.env.NETLIFY === "true"
19
- );
20
- }
21
-
22
16
  function scheduledFunctionRuntime() {
23
17
  return globalThis.__AGENT_NATIVE_BRAIN_SCHEDULED_RUNTIME__ === true;
24
18
  }
@@ -35,7 +29,7 @@ function headerMatchesSecret(header: string | undefined, secret: string) {
35
29
  export default defineEventHandler(async (event) => {
36
30
  const secret = process.env.BRAIN_JOBS_SECRET?.trim(); // guard:allow-env-credential — deployment-level scheduler route secret
37
31
  const scheduled = scheduledFunctionRuntime();
38
- if (!secret && productionLike() && !scheduled) {
32
+ if (!secret && !scheduled) {
39
33
  throw createError({
40
34
  statusCode: 503,
41
35
  statusMessage: "BRAIN_JOBS_SECRET is required",
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","auth","core-routes","db","event-rules","oauth-refresh","otel","transactional-emails"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","auth","otel"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","auth","calendar-jobs","config","core-routes","db","feature-flags","labs","onboarding","onboarding-calendar","otel","transactional-email-catalog"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }
@@ -1,8 +1,10 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
+ import { useLab } from "@agent-native/core/client/labs";
2
3
  import { useOrgRole } from "@agent-native/core/client/org";
3
4
  import { docsUrl } from "@agent-native/core/shared";
4
5
  import { useCommandMenuShortcut } from "@agent-native/toolkit/app/shared";
5
6
  import { CommandMenu } from "@agent-native/toolkit/app/shared";
7
+ import { CLIPS_SCREENSHOTS } from "@shared/labs";
6
8
  import {
7
9
  IconArchive,
8
10
  IconPhoto,
@@ -393,6 +395,7 @@ export function ClipsCommandMenu({
393
395
  const location = useLocation();
394
396
  const navigate = useNavigate();
395
397
  const t = useT();
398
+ const screenshotsEnabled = useLab(CLIPS_SCREENSHOTS);
396
399
  const { canManageOrg } = useOrgRole();
397
400
  const context = useMemo(
398
401
  () => commandRouteContext(location.pathname),
@@ -614,13 +617,15 @@ export function ClipsCommandMenu({
614
617
  {t("navigation.dictate")}
615
618
  <CommandMenu.Shortcut>G D</CommandMenu.Shortcut>
616
619
  </CommandMenu.Item>
617
- <CommandMenu.Item
618
- onSelect={() => void navigate("/screenshots")}
619
- keywords={["screenshots", "screenshot", "images", "stills"]}
620
- >
621
- <IconPhoto size={16} />
622
- {t("navigation.screenshots")}
623
- </CommandMenu.Item>
620
+ {screenshotsEnabled && (
621
+ <CommandMenu.Item
622
+ onSelect={() => void navigate("/screenshots")}
623
+ keywords={["screenshots", "screenshot", "images", "stills"]}
624
+ >
625
+ <IconPhoto size={16} />
626
+ {t("navigation.screenshots")}
627
+ </CommandMenu.Item>
628
+ )}
624
629
  <CommandMenu.Item
625
630
  onSelect={() => void navigate("/archive")}
626
631
  keywords={["archive"]}
@@ -1,4 +1,6 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
+ import { useLab } from "@agent-native/core/client/labs";
3
+ import { CLIPS_SCREENSHOTS } from "@shared/labs";
2
4
  import { IconChevronDown, IconUpload } from "@tabler/icons-react";
3
5
  import { useState } from "react";
4
6
 
@@ -63,6 +65,7 @@ export function ImportMenu({
63
65
  variant = "outline",
64
66
  }: ImportMenuProps) {
65
67
  const t = useT();
68
+ const screenshotsEnabled = useLab(CLIPS_SCREENSHOTS);
66
69
  const { input, openUploadPicker } = useUploadVideoPicker();
67
70
  const [loomDialogOpen, setLoomDialogOpen] = useState(false);
68
71
 
@@ -118,7 +121,7 @@ export function ImportMenu({
118
121
  {t("preRecord.uploadVideo")}
119
122
  </DropdownMenuItem>
120
123
  ) : null}
121
- {onScreenshot ? (
124
+ {onScreenshot && screenshotsEnabled ? (
122
125
  <DropdownMenuItem
123
126
  disabled={screenshotPending}
124
127
  onSelect={(event) => {
@@ -6,7 +6,7 @@ import {
6
6
  import { useT } from "@agent-native/core/client/i18n";
7
7
  import { normalizeDocumentTitle } from "@agent-native/core/shared";
8
8
  import { FileStorageSetupPopover } from "@agent-native/toolkit/app/chat/FileStorageSetupPopover";
9
- import { IconAlertTriangle } from "@tabler/icons-react";
9
+ import { IconAlertTriangle, IconPhoto } from "@tabler/icons-react";
10
10
  import {
11
11
  type DragEvent,
12
12
  type ReactElement,
@@ -59,7 +59,7 @@ import { resolveVideoMimeType } from "@/lib/video-metadata";
59
59
 
60
60
  import { BulkActionToolbar, type BulkMoveTarget } from "./bulk-action-toolbar";
61
61
  import { DroppedUploadCard } from "./dropped-upload-card";
62
- import { EmptyState } from "./empty-state";
62
+ import { AppEmptyState, EmptyState } from "./empty-state";
63
63
  import { FilterChips, type FilterChip } from "./filter-chips";
64
64
  import { FolderCard } from "./folder-card";
65
65
  import { buildLibraryActionHrefs } from "./library-action-hrefs";
@@ -757,7 +757,7 @@ export function LibraryGrid({
757
757
  />
758
758
  )}
759
759
  <div className="ms-auto flex shrink-0 items-center gap-2 lg:col-start-3 lg:ms-0 lg:justify-self-end">
760
- {!isEmptyState && extraActions}
760
+ {(!isEmptyState || kind === "image") && extraActions}
761
761
  {!isEmptyState && (
762
762
  <SortMenu value={sort} onChange={handleSortChange} />
763
763
  )}
@@ -834,11 +834,18 @@ export function LibraryGrid({
834
834
  ) : recordings.length === 0 &&
835
835
  visibleFolders.length === 0 &&
836
836
  uploads.length === 0 ? (
837
- <EmptyState
838
- kind={resolvedEmptyKind}
839
- spaceId={spaceId}
840
- folderId={folderId}
841
- />
837
+ kind === "image" ? (
838
+ <AppEmptyState
839
+ icon={IconPhoto}
840
+ title={t("navigation.screenshots")}
841
+ />
842
+ ) : (
843
+ <EmptyState
844
+ kind={resolvedEmptyKind}
845
+ spaceId={spaceId}
846
+ folderId={folderId}
847
+ />
848
+ )
842
849
  ) : (
843
850
  <div className="flex flex-col gap-8">
844
851
  {visibleFolders.length > 0 && (
@@ -12,7 +12,11 @@ import {
12
12
  AppSidebarFooter,
13
13
  AppSidebarHeader,
14
14
  } from "@agent-native/toolkit/app/shared";
15
- import { CLIPS_MEETINGS, CLIPS_VOICE_DICTATION } from "@shared/labs";
15
+ import {
16
+ CLIPS_MEETINGS,
17
+ CLIPS_SCREENSHOTS,
18
+ CLIPS_VOICE_DICTATION,
19
+ } from "@shared/labs";
16
20
  import {
17
21
  IconInbox,
18
22
  IconPhoto,
@@ -195,6 +199,7 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
195
199
  const t = useT();
196
200
  const isRecordingRoute = location.pathname.startsWith("/r/");
197
201
  const meetingsLabEnabled = useLab(CLIPS_MEETINGS);
202
+ const screenshotsLabEnabled = useLab(CLIPS_SCREENSHOTS);
198
203
  const voiceDictationLabEnabled = useLab(CLIPS_VOICE_DICTATION);
199
204
  const recordingScope = useMemo(() => {
200
205
  const match = location.pathname.match(/^\/(?:r|share)\/([^/]+)/);
@@ -564,7 +569,10 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
564
569
  ];
565
570
 
566
571
  const primaryNavItems = navItems.filter(
567
- ({ to }) => to !== "/archive" && to !== "/trash",
572
+ ({ to }) =>
573
+ to !== "/archive" &&
574
+ to !== "/trash" &&
575
+ (to !== "/screenshots" || screenshotsLabEnabled),
568
576
  );
569
577
  const lifecycleNavItems = navItems.filter(
570
578
  ({ to }) => to === "/archive" || to === "/trash",
@@ -64,6 +64,7 @@ import {
64
64
  PopoverTrigger,
65
65
  PopoverContent,
66
66
  } from "@/components/ui/popover";
67
+ import { Switch } from "@/components/ui/switch";
67
68
  import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
68
69
  import { cn } from "@/lib/utils";
69
70
 
@@ -755,7 +756,7 @@ function RecordingAdvancedAccess({
755
756
  >
756
757
  {t("embedRoute.passwordRequired")}
757
758
  </Label>
758
- <ViewerSwitch
759
+ <Switch
759
760
  id="share-password-required"
760
761
  checked={passwordEnabled}
761
762
  disabled={!canEdit || updateRecording.isPending}
@@ -765,7 +766,7 @@ function RecordingAdvancedAccess({
765
766
 
766
767
  {passwordEnabled ? (
767
768
  <>
768
- <div className="flex gap-2">
769
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] gap-2">
769
770
  <Input
770
771
  size="sm"
771
772
  type="text"
@@ -778,12 +779,10 @@ function RecordingAdvancedAccess({
778
779
  ? t("playerSettings.passwordSetPlaceholder")
779
780
  : t("playerSettings.passwordInputPlaceholder")
780
781
  }
781
- className="min-w-0"
782
782
  />
783
783
  <Button
784
784
  type="button"
785
785
  size="sm"
786
- className="shrink-0"
787
786
  disabled={
788
787
  !canEdit || updateRecording.isPending || !password.trim()
789
788
  }
@@ -815,7 +814,7 @@ function RecordingAdvancedAccess({
815
814
  <Label htmlFor="share-expiry" className="text-sm font-normal">
816
815
  {t("shareDialog.linkExpires")}
817
816
  </Label>
818
- <div className="flex gap-2">
817
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] gap-2">
819
818
  <Input
820
819
  id="share-expiry"
821
820
  size="sm"
@@ -825,12 +824,10 @@ function RecordingAdvancedAccess({
825
824
  onChange={(event) =>
826
825
  setExpiryDraft(fromDatetimeLocal(event.target.value))
827
826
  }
828
- className="min-w-0"
829
827
  />
830
828
  <Button
831
829
  type="button"
832
830
  size="sm"
833
- className="shrink-0"
834
831
  disabled={!canEdit || updateRecording.isPending}
835
832
  onClick={() => {
836
833
  const nextExpiry = expiryDraft || null;
@@ -92,7 +92,7 @@ export function ShareAdvancedPopover({
92
92
  align="start"
93
93
  sideOffset={6}
94
94
  onOpenAutoFocus={(event) => event.preventDefault()}
95
- className="w-72 space-y-3 p-3"
95
+ className="w-80 max-w-[calc(100vw-2rem)] space-y-3 p-3"
96
96
  >
97
97
  {children}
98
98
  </PopoverContent>
@@ -1,7 +1,11 @@
1
1
  import { useT } from "@agent-native/core/client/i18n";
2
+ import { useLabState } from "@agent-native/core/client/labs";
3
+ import { CLIPS_SCREENSHOTS } from "@shared/labs";
4
+ import { Navigate } from "react-router";
2
5
 
3
6
  import { LibraryGrid } from "@/components/library/library-grid";
4
7
  import { ScreenshotPrimaryAction } from "@/components/library/library-primary-actions";
8
+ import { Button } from "@/components/ui/button";
5
9
  import enMessages from "@/i18n/en-US";
6
10
 
7
11
  export function meta() {
@@ -16,6 +20,18 @@ export function meta() {
16
20
  */
17
21
  export default function ScreenshotsRoute() {
18
22
  const t = useT();
23
+ const lab = useLabState(CLIPS_SCREENSHOTS.key);
24
+ if (lab.isLoading) return null;
25
+ if (lab.isError) {
26
+ return (
27
+ <div role="alert" className="flex flex-col items-center gap-4 py-20">
28
+ <p>{t("sharePage.somethingWentWrong")}</p>
29
+ <Button onClick={lab.refetch}>{t("libraryGrid.retry")}</Button>
30
+ </div>
31
+ );
32
+ }
33
+ if (!lab.isSuccess) return null;
34
+ if (!lab.enabled) return <Navigate replace to="/library" />;
19
35
  return (
20
36
  <LibraryGrid
21
37
  view="library"
@@ -4,7 +4,11 @@ import {
4
4
  SettingsTabsPage,
5
5
  useAgentSettingsTabs,
6
6
  } from "@agent-native/toolkit/app/settings";
7
- import { CLIPS_LABS, CLIPS_VOICE_DICTATION } from "@shared/labs";
7
+ import {
8
+ CLIPS_LABS,
9
+ CLIPS_SCREENSHOTS,
10
+ CLIPS_VOICE_DICTATION,
11
+ } from "@shared/labs";
8
12
  import { useMemo } from "react";
9
13
 
10
14
  import { useClipsSettingsRedesign } from "@/components/settings/clips-settings-redesign";
@@ -54,6 +58,13 @@ export default function SettingsIndexRoute() {
54
58
  description: t("settings.labLookbackContextDescription"),
55
59
  };
56
60
  }
61
+ if (lab.key === CLIPS_SCREENSHOTS.key) {
62
+ return {
63
+ ...lab,
64
+ displayName: t("navigation.screenshots"),
65
+ description: t("preRecord.takeScreenshot"),
66
+ };
67
+ }
57
68
  if (lab.key === CLIPS_VOICE_DICTATION.key) {
58
69
  return {
59
70
  ...lab,
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-28
4
+ ---
5
+
6
+ Fixed the Share → Advanced popover so the Save buttons fit and the password toggle matches other switches
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: changed
3
+ date: 2026-10-29
4
+ ---
5
+
6
+ Screenshots are available when enabled in Labs.
@@ -62,10 +62,19 @@ export const CLIPS_LOOKBACK_CONTEXT = defineLab({
62
62
  keywords: "rewind lookback context history earlier screen before recording",
63
63
  });
64
64
 
65
+ export const CLIPS_SCREENSHOTS = defineLab({
66
+ key: "clips.screenshots",
67
+ displayName: "Screenshots",
68
+ description: "Take screenshots.",
69
+ defaultEnabled: false,
70
+ keywords: "screenshots screenshot capture image",
71
+ });
72
+
65
73
  export const CLIPS_LABS = defineLabs([
66
74
  CLIPS_VIDEO_EDITING,
67
75
  CLIPS_MEETINGS,
68
76
  CLIPS_VOICE_DICTATION,
69
77
  CLIPS_RESILIENT_RECORDING,
70
78
  CLIPS_LOOKBACK_CONTEXT,
79
+ CLIPS_SCREENSHOTS,
71
80
  ]);
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","auth","builder-private-payload-cleanup","collab","content-webhooks","core-routes","creative-context","db","feature-flags","github-source-sync","labs","live-body-backfill","onboarding","otel","suggested-edits","transactional-emails"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }
@@ -4,7 +4,19 @@ import {
4
4
  isActionContractError,
5
5
  } from "@agent-native/core/action";
6
6
  import { assertAccess } from "@agent-native/core/sharing";
7
- import { and, eq, exists, isNull, lt, notInArray, or } from "drizzle-orm";
7
+ import {
8
+ and,
9
+ eq,
10
+ exists,
11
+ gte,
12
+ inArray,
13
+ isNull,
14
+ lt,
15
+ ne,
16
+ notInArray,
17
+ or,
18
+ sql,
19
+ } from "drizzle-orm";
8
20
  import { z } from "zod";
9
21
 
10
22
  import { getDb, schema } from "../server/db/index.js";
@@ -148,6 +160,11 @@ export interface ExecuteBuilderSourceExecutionDeps {
148
160
  now: string;
149
161
  staleBefore: string;
150
162
  attemptToken: string;
163
+ /**
164
+ * The existing entry this execution overwrites. Its claim is refused while
165
+ * another source's push to the same entry holds a live claim.
166
+ */
167
+ builderEntry?: { model: string; entryId: string } | null;
151
168
  }) => Promise<boolean>;
152
169
  renewExecutionClaim: (args: {
153
170
  executionId: string;
@@ -313,6 +330,55 @@ export function builderExecutionConflict(message: string) {
313
330
  return Object.assign(new Error(message), { statusCode: 409 });
314
331
  }
315
332
 
333
+ // Collections are separate sources, so the database lock a claim takes does not
334
+ // keep two collections bound to the same Builder model apart. The live preflight
335
+ // only protects a push if no other Content push to the entry can land between
336
+ // its read and its write, so a push holds the entry from claim to write. A
337
+ // write whose outcome is unknown may still land, so it holds the entry too,
338
+ // until the claim lease outlasts any request still in flight.
339
+ async function builderEntryHasOtherLiveClaim(
340
+ tx: ReturnType<typeof getDb>,
341
+ args: {
342
+ model: string;
343
+ entryId: string;
344
+ executionId: string;
345
+ staleBefore: string;
346
+ },
347
+ ) {
348
+ await tx.execute(
349
+ sql`SELECT pg_advisory_xact_lock(hashtextextended(${`content-builder-entry-write:${args.model}:${args.entryId}`}, 0::bigint))`,
350
+ );
351
+ const executions = schema.contentDatabaseSourceExecutions;
352
+ const changeSets = schema.contentDatabaseSourceChangeSets;
353
+ const rows = schema.contentDatabaseSourceRows;
354
+ const sources = schema.contentDatabaseSources;
355
+ const claims = await tx
356
+ .select({ id: executions.id })
357
+ .from(sources)
358
+ .innerJoin(rows, eq(rows.sourceId, sources.id))
359
+ .innerJoin(
360
+ changeSets,
361
+ and(
362
+ eq(changeSets.sourceId, sources.id),
363
+ eq(changeSets.databaseItemId, rows.databaseItemId),
364
+ ),
365
+ )
366
+ .innerJoin(executions, eq(executions.changeSetId, changeSets.id))
367
+ .where(
368
+ and(
369
+ eq(sources.sourceType, "builder-cms"),
370
+ eq(sources.sourceTable, args.model),
371
+ eq(rows.sourceRowId, args.entryId),
372
+ eq(executions.sourceId, sources.id),
373
+ ne(executions.id, args.executionId),
374
+ inArray(executions.state, ["running", "reconciliation_required"]),
375
+ gte(executions.updatedAt, args.staleBefore),
376
+ ),
377
+ )
378
+ .limit(1);
379
+ return claims.length > 0;
380
+ }
381
+
316
382
  function isReclaimableRunningExecution(
317
383
  execution: BuilderSourceExecutionRecord,
318
384
  now: string,
@@ -1286,6 +1352,16 @@ export function realExecutionDeps(
1286
1352
  )
1287
1353
  .limit(1);
1288
1354
  if (!scope) return null;
1355
+ if (
1356
+ args.builderEntry &&
1357
+ (await builderEntryHasOtherLiveClaim(transactionDb, {
1358
+ ...args.builderEntry,
1359
+ executionId: args.executionId,
1360
+ staleBefore: args.staleBefore,
1361
+ }))
1362
+ ) {
1363
+ return "entry_busy" as const;
1364
+ }
1289
1365
  await lockContentDatabaseMutation(transactionDb, scope.databaseId);
1290
1366
  const updated = await tx
1291
1367
  .update(schema.contentDatabaseSourceExecutions)
@@ -1373,6 +1449,13 @@ export function realExecutionDeps(
1373
1449
  }
1374
1450
  throw error;
1375
1451
  }
1452
+ if (result === "entry_busy") {
1453
+ await discardUncommittedPayload(prepared);
1454
+ fail(
1455
+ "Another Content collection is pushing to this Builder entry. Try again in a moment.",
1456
+ { errorCode: "builder_entry_write_in_progress", statusCode: 409 },
1457
+ );
1458
+ }
1376
1459
  if (builderExecutionAffectedRows(result) === 0) {
1377
1460
  await discardUncommittedPayload(prepared);
1378
1461
  }
@@ -2161,6 +2244,9 @@ export async function executeBuilderSourceExecutionWithDeps(
2161
2244
  now,
2162
2245
  staleBefore: staleRunningCutoff(now),
2163
2246
  attemptToken,
2247
+ builderEntry: preflightEntryId
2248
+ ? { model: plan.payload.target.model, entryId: preflightEntryId }
2249
+ : null,
2164
2250
  });
2165
2251
  if (!claimed) {
2166
2252
  throw builderExecutionConflict("Builder execution is already running.");
@@ -2191,9 +2277,10 @@ export async function executeBuilderSourceExecutionWithDeps(
2191
2277
  let writePayload: BuilderCmsExecutionPayload = validatedPayload;
2192
2278
  if (preflightEntryId) {
2193
2279
  // This check runs after the claim so it sits as close to the write as
2194
- // possible. A Builder save that lands after this read and before the
2195
- // write below is still overwritten: the check narrows that race but
2196
- // cannot close it without a conditional write on Builder's side.
2280
+ // possible. The claim keeps other Content pushes to this entry out, but a
2281
+ // save made in Builder that lands after this read and before the write
2282
+ // below is still overwritten: closing that needs a conditional write on
2283
+ // Builder's side.
2197
2284
  let liveState: BuilderCmsEntryLiveState;
2198
2285
  try {
2199
2286
  liveState = await deps.readLiveEntry({
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-27
4
+ ---
5
+
6
+ Two collections connected to the same Builder model can no longer push over each other's changes to the same entry at the same moment.
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","auth","core-routes","db","otel"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }
@@ -26,8 +26,8 @@ only when the available context leaves materially different choices.
26
26
 
27
27
  - Create the design if needed, then author complete email HTML with
28
28
  `create-file` (`fileType: "html"`) or `generate-design` (`devices: []`, a
29
- 600px-wide `canvasFrames` placement). Use optional `frameType: "hubspot-email"`
30
- with `<meta name="agent-native:frame-type" content="hubspot-email">` in the
29
+ 600px-wide `canvasFrames` placement). Put
30
+ `<meta name="agent-native:frame-type" content="hubspot-email">` in the
31
31
  head to retain email authoring defaults on future edits. Include the leading
32
32
  HubSpot header comment containing `templateType: email` and
33
33
  `isAvailableForNewContent: true` so new emails are selectable. The leading
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","assets-mcp","auth","builder-host-embed-headers","collab","config","core-routes","creative-context","db","e2e-attachment-storage","feature-flags","getting-started","journey-canvas-stage-cleanup","labs","local-import-asset-upload","otel","review"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }