@agent-native/core 0.146.5 → 0.146.7

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 (38) hide show
  1. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +13 -9
  2. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +69 -33
  3. package/corpus/templates/calendar/app/root.tsx +0 -1
  4. package/corpus/templates/clips/app/root.tsx +0 -1
  5. package/corpus/templates/content/app/root.tsx +0 -1
  6. package/dist/collab/routes.d.ts +1 -1
  7. package/dist/collab/struct-routes.d.ts +1 -1
  8. package/dist/deploy/build.d.ts +21 -0
  9. package/dist/deploy/build.js +185 -10
  10. package/dist/jobs/background-automation-runner.d.ts +1 -1
  11. package/dist/jobs/background-automation-runner.js +1 -1
  12. package/dist/jobs/scheduler-health.d.ts +4 -0
  13. package/dist/jobs/scheduler-health.js +33 -0
  14. package/dist/localization/server.d.ts +0 -1
  15. package/dist/localization/server.js +3 -1
  16. package/dist/mcp-client/index.d.ts +1 -1
  17. package/dist/mcp-client/index.js +1 -1
  18. package/dist/mcp-client/routes.d.ts +11 -0
  19. package/dist/mcp-client/routes.js +26 -1
  20. package/dist/notifications/routes.d.ts +1 -1
  21. package/dist/observability/routes.d.ts +5 -5
  22. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  23. package/dist/provider-api/actions/provider-api.d.ts +5 -5
  24. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  25. package/dist/resources/handlers.d.ts +1 -1
  26. package/dist/secrets/routes.d.ts +9 -9
  27. package/dist/server/agent-chat/mcp-glue.js +12 -3
  28. package/dist/server/agent-chat-plugin.js +73 -48
  29. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  30. package/dist/server/auth.js +7 -0
  31. package/dist/server/http-response-telemetry.js +112 -5
  32. package/dist/server/release-migrations.js +2 -0
  33. package/dist/server/ssr-handler.js +8 -0
  34. package/dist/server/transcribe-voice.d.ts +1 -1
  35. package/dist/templates/workspace-core/.agents/skills/performance/SKILL.md +54 -3
  36. package/dist/vite/client.js +5 -1
  37. package/package.json +1 -1
  38. package/src/templates/workspace-core/.agents/skills/performance/SKILL.md +54 -3
@@ -201,21 +201,13 @@ export async function queueFirstPartyAnalyticsBigQueryBackfill(
201
201
  const id = jobId(scope.orgId ?? "");
202
202
  if (!scope.orgId)
203
203
  throw new Error("BigQuery backfill requires an organization");
204
- const conflictClause =
205
- requestedBatchSize === undefined
206
- ? "ON CONFLICT (id) DO NOTHING"
207
- : `ON CONFLICT (id) DO UPDATE SET batch_size = CASE
208
- WHEN batch_size < excluded.batch_size
209
- THEN excluded.batch_size
210
- ELSE batch_size
211
- END`;
212
204
  await executor().execute({
213
205
  sql: `INSERT INTO ${JOB_TABLE} (
214
206
  id, org_id, owner_email, table_ref, batch_size, backfill_cursor,
215
207
  status, copied_count, lease_token, lease_expires_at, next_run_at,
216
208
  last_error, completed_at, updated_at
217
209
  ) VALUES (?, ?, ?, ?, ?, ?, 'pending', 0, NULL, NULL, ?, NULL, NULL, ?)
218
- ${conflictClause}`,
210
+ ON CONFLICT (id) DO NOTHING`,
219
211
  args: [
220
212
  id,
221
213
  scope.orgId,
@@ -229,6 +221,18 @@ export async function queueFirstPartyAnalyticsBigQueryBackfill(
229
221
  timeoutMs: 5_000,
230
222
  maxAttempts: 1,
231
223
  });
224
+ if (requestedBatchSize !== undefined) {
225
+ const batchSize = boundedBatchSize(requestedBatchSize);
226
+ await executor().execute({
227
+ sql: `UPDATE ${JOB_TABLE}
228
+ SET batch_size = ?
229
+ WHERE id = ?
230
+ AND batch_size < ?`,
231
+ args: [batchSize, id, batchSize],
232
+ timeoutMs: 5_000,
233
+ maxAttempts: 1,
234
+ });
235
+ }
232
236
  const job = await getFirstPartyAnalyticsBigQueryBackfillJob(scope);
233
237
  if (!job) throw new Error("BigQuery backfill job was not persisted");
234
238
  if (job.table !== table) {
@@ -1031,7 +1031,7 @@ function backfillBranchSql(
1031
1031
  ? [cursor.receivedAt, cursor.receivedAt, cursor.id]
1032
1032
  : [];
1033
1033
  return {
1034
- sql: `SELECT ${FIRST_PARTY_ANALYTICS_BACKFILL_COLUMNS.join(", ")}
1034
+ sql: `SELECT id, received_at
1035
1035
  FROM analytics_events
1036
1036
  WHERE ${predicate}${cursorSql ? ` AND ${cursorSql}` : ""}
1037
1037
  ORDER BY received_at ASC, id ASC LIMIT ?`,
@@ -1039,32 +1039,20 @@ function backfillBranchSql(
1039
1039
  };
1040
1040
  }
1041
1041
 
1042
- function backfillScopedRowsSql(
1043
- branches: Array<{ predicate: string; args: string[] }>,
1044
- cursor: FirstPartyAnalyticsBackfillCursor,
1045
- limit: number,
1046
- ): { sql: string; args: Array<string | number> } {
1047
- const branchQueries = branches.map((branch) =>
1048
- backfillBranchSql(branch.predicate, branch.args, cursor),
1049
- );
1050
- const columns = FIRST_PARTY_ANALYTICS_BACKFILL_COLUMNS.join(", ");
1042
+ function backfillRowsByIdsSql(ids: string[]): {
1043
+ sql: string;
1044
+ args: string[];
1045
+ } {
1051
1046
  return {
1052
- sql: `WITH ${branchQueries
1053
- .map(
1054
- (_branch, index) =>
1055
- `backfill_branch_${index} AS (${branchQueries[index]!.sql})`,
1056
- )
1057
- .join(", ")}
1058
- SELECT ${columns}
1059
- FROM (${branchQueries.map((_branch, index) => `SELECT ${columns} FROM backfill_branch_${index}`).join(" UNION ALL ")}) AS selected_events
1060
- ORDER BY received_at ASC, id ASC LIMIT ?`,
1061
- args: [
1062
- ...branchQueries.flatMap((branch) => [...branch.args, limit]),
1063
- limit,
1064
- ],
1047
+ sql: `SELECT ${FIRST_PARTY_ANALYTICS_BACKFILL_COLUMNS.join(", ")}
1048
+ FROM analytics_events
1049
+ WHERE id IN (${ids.map(() => "?").join(", ")})`,
1050
+ args: ids,
1065
1051
  };
1066
1052
  }
1067
1053
 
1054
+ const MAX_SQLITE_BIND_VARIABLES = 900;
1055
+
1068
1056
  function backfillRowCursor(
1069
1057
  row: Record<string, unknown>,
1070
1058
  ): FirstPartyAnalyticsBackfillCursor {
@@ -1081,6 +1069,18 @@ function backfillRowCursor(
1081
1069
  return { receivedAt, id };
1082
1070
  }
1083
1071
 
1072
+ function compareBackfillRows(
1073
+ left: Record<string, unknown>,
1074
+ right: Record<string, unknown>,
1075
+ ): number {
1076
+ const leftCursor = backfillRowCursor(left);
1077
+ const rightCursor = backfillRowCursor(right);
1078
+ return (
1079
+ leftCursor.receivedAt.localeCompare(rightCursor.receivedAt) ||
1080
+ leftCursor.id.localeCompare(rightCursor.id)
1081
+ );
1082
+ }
1083
+
1084
1084
  export async function backfillFirstPartyAnalyticsBatch(
1085
1085
  scope: FirstPartyAnalyticsScope,
1086
1086
  cursor: string | null,
@@ -1108,15 +1108,22 @@ export async function backfillFirstPartyAnalyticsBatch(
1108
1108
  },
1109
1109
  ];
1110
1110
  const rows: Record<string, unknown>[] = [];
1111
- const scoped = backfillScopedRowsSql(branches, parsedCursor, boundedLimit);
1112
- const result = await db.execute({
1113
- sql: scoped.sql,
1114
- args: scoped.args,
1115
- timeoutMs: 20_000,
1116
- maxAttempts: 1,
1117
- });
1118
- rows.push(...(result.rows as Record<string, unknown>[]));
1119
- const selectedRows = rows;
1111
+ for (const branch of branches) {
1112
+ const scoped = backfillBranchSql(
1113
+ branch.predicate,
1114
+ branch.args,
1115
+ parsedCursor,
1116
+ );
1117
+ const result = await db.execute({
1118
+ sql: scoped.sql,
1119
+ args: [...scoped.args, boundedLimit],
1120
+ timeoutMs: 20_000,
1121
+ maxAttempts: 1,
1122
+ });
1123
+ rows.push(...(result.rows as Record<string, unknown>[]));
1124
+ }
1125
+ rows.sort(compareBackfillRows);
1126
+ const selectedRows = rows.slice(0, boundedLimit);
1120
1127
  if (!selectedRows.length) {
1121
1128
  return {
1122
1129
  nextCursor: serializeBackfillCursor(parsedCursor),
@@ -1125,7 +1132,36 @@ export async function backfillFirstPartyAnalyticsBatch(
1125
1132
  };
1126
1133
  }
1127
1134
 
1128
- const selectedEvents = selectedRows;
1135
+ const selectedIds = selectedRows.map((row) => backfillRowCursor(row).id);
1136
+ const hydratedRows: Record<string, unknown>[] = [];
1137
+ for (
1138
+ let offset = 0;
1139
+ offset < selectedIds.length;
1140
+ offset += MAX_SQLITE_BIND_VARIABLES
1141
+ ) {
1142
+ const hydratedQuery = backfillRowsByIdsSql(
1143
+ selectedIds.slice(offset, offset + MAX_SQLITE_BIND_VARIABLES),
1144
+ );
1145
+ const hydratedResult = await db.execute({
1146
+ sql: hydratedQuery.sql,
1147
+ args: hydratedQuery.args,
1148
+ timeoutMs: 20_000,
1149
+ maxAttempts: 1,
1150
+ });
1151
+ hydratedRows.push(...(hydratedResult.rows as Record<string, unknown>[]));
1152
+ }
1153
+ const hydratedById = new Map(
1154
+ hydratedRows.map((row) => [backfillRowCursor(row).id, row]),
1155
+ );
1156
+ const selectedEvents = selectedIds.map((id) => {
1157
+ const row = hydratedById.get(id);
1158
+ if (!row) {
1159
+ throw new Error(
1160
+ `First-party analytics backfill row ${id} disappeared before hydration`,
1161
+ );
1162
+ }
1163
+ return row;
1164
+ });
1129
1165
  await insertFirstPartyAnalyticsRows(selectedEvents, configuredTable);
1130
1166
  const lastCursor = backfillRowCursor(selectedRows[selectedRows.length - 1]!);
1131
1167
  return {
@@ -95,7 +95,6 @@ export function Layout({ children }: { children: React.ReactNode }) {
95
95
  const localeInitScript = getLocaleInitScript({
96
96
  locale: loaderData.locale,
97
97
  preference: loaderData.preference,
98
- messages: loaderData.messages,
99
98
  });
100
99
 
101
100
  return (
@@ -130,7 +130,6 @@ export function Layout({ children }: { children: React.ReactNode }) {
130
130
  const localeInitScript = getHydrationStableLocaleInitScript({
131
131
  locale: loaderData.locale,
132
132
  preference: loaderData.preference,
133
- messages: loaderData.messages,
134
133
  });
135
134
 
136
135
  return (
@@ -188,7 +188,6 @@ export function Layout({ children }: { children: React.ReactNode }) {
188
188
  const localeInitScript = getLocaleInitScript({
189
189
  locale: loaderData.locale,
190
190
  preference: loaderData.preference,
191
- messages: loaderData.messages,
192
191
  });
193
192
 
194
193
  return (
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- ok?: undefined;
30
29
  error: string;
30
+ ok?: undefined;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- error: string;
17
16
  ok?: undefined;
17
+ error: string;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -74,6 +74,14 @@ export { copyDir };
74
74
  export declare function isServerlessNativePlatformPackage(packageName: string): boolean;
75
75
  type ServerlessFfmpegStaticArch = "arm64" | "x64";
76
76
  export declare function shouldBundleFfmpegStaticForServerless(hostPlatform?: NodeJS.Platform, hostArch?: NodeJS.Architecture, targetArch?: ServerlessFfmpegStaticArch | null): boolean;
77
+ /**
78
+ * Which dependency gives this app a path to the serverless browser runtime, or
79
+ * null when it has none. Resolution cannot answer this: findInstalledPackageRoot
80
+ * falls back to an ancestor + .pnpm store walk, so in a workspace every app
81
+ * "resolves" the sibling package that actually declares Chromium and ships its
82
+ * ~80MB into every emitted function.
83
+ */
84
+ export declare function findServerlessBrowserRuntimeConsumer(projectCwd?: string): string | null;
77
85
  export declare function findInstalledPackageRoot(packageName: string, nodeModulesRoots: string[], fromPackageDir?: string): string | null;
78
86
  export declare function copyInstalledBrowserRuntimePackages(serverDir: string | undefined, projectCwd?: string): number;
79
87
  export declare function findInstalledFfmpegStaticPackage(nodeModulesRoots: string[]): string | null;
@@ -238,6 +246,19 @@ export declare function writeSingleTemplateNetlifyRedirects(projectCwd: string):
238
246
  * though the server bundle never imports Electron at runtime.
239
247
  */
240
248
  export declare function sanitizeServerlessFunctionPackageManifest(functionDir: string | undefined): void;
249
+ /**
250
+ * Nitro's dependency tracer is not subject to the copy-time filters above, so
251
+ * it ships every prebuild of a multi-platform native package plus whatever the
252
+ * local dev server left in `data/`. Prune both from the function dir before it
253
+ * is cloned for the extra functions, since each clone is zipped and uploaded in
254
+ * full.
255
+ *
256
+ * Only prunes inside a directory that still holds a runnable prebuild under the
257
+ * `isServerlessNativePlatformPackage` naming (`@libsql`, `@resvg`). Packages
258
+ * that name prebuilds differently — `@img/sharp-linux-x64` has no gnu/musl
259
+ * suffix — read as entirely dead and must be left alone.
260
+ */
261
+ export declare function pruneServerlessFunctionDeadWeight(functionDir: string | undefined): number;
241
262
  /**
242
263
  * Build for any non-Cloudflare preset using Nitro's programmatic build API.
243
264
  * Handles netlify, vercel, deno_deploy, aws-lambda, and all other targets.
@@ -1925,6 +1925,13 @@ const SERVERLESS_BROWSER_RUNTIME_PACKAGES = [
1925
1925
  "@sparticuz/chromium",
1926
1926
  "playwright-core",
1927
1927
  ];
1928
+ const SERVERLESS_BROWSER_RUNTIME_CONSUMER = "@agent-native/creative-context";
1929
+ const PACKAGE_DEPENDENCY_FIELDS = [
1930
+ "dependencies",
1931
+ "optionalDependencies",
1932
+ "devDependencies",
1933
+ "peerDependencies",
1934
+ ];
1928
1935
  // Serverless functions only ever run on 64-bit Linux. The darwin/win32/android
1929
1936
  // and 32-bit-arm prebuilds of these native packages are ~100MB that can never
1930
1937
  // execute there, and Netlify copies the whole server dir again for every extra
@@ -1941,6 +1948,11 @@ const SERVERLESS_NATIVE_PACKAGE_SUFFIXES = [
1941
1948
  export function isServerlessNativePlatformPackage(packageName) {
1942
1949
  return SERVERLESS_NATIVE_PACKAGE_SUFFIXES.some((suffix) => packageName.endsWith(suffix));
1943
1950
  }
1951
+ // Names a prebuild package by the platform it targets: `darwin-arm64`,
1952
+ // `resvg-js-win32-ia32-msvc`, `linux-x64-gnu`. Matching only says "this is a
1953
+ // per-platform prebuild"; isServerlessNativePlatformPackage decides whether it
1954
+ // can run in the function.
1955
+ const SERVERLESS_PLATFORM_PACKAGE_NAME = /(?:^|-)(?:darwin|win32|linux|android|freebsd)-[a-z0-9]+(?:-[a-z0-9]+)?$/;
1944
1956
  const FFMPEG_STATIC_BINARY_NAMES = process.platform === "win32" ? ["ffmpeg.exe", "ffmpeg"] : ["ffmpeg"];
1945
1957
  const SERVERLESS_FFMPEG_STATIC_PLATFORM = "linux";
1946
1958
  const SERVERLESS_FFMPEG_STATIC_ARCHES = new Set([
@@ -1993,6 +2005,37 @@ function readPackageManifest(packageDir) {
1993
2005
  }
1994
2006
  return manifest;
1995
2007
  }
2008
+ function manifestDeclaresDependency(manifest, packageName) {
2009
+ if (!manifest)
2010
+ return false;
2011
+ return PACKAGE_DEPENDENCY_FIELDS.some((field) => {
2012
+ const deps = manifest[field];
2013
+ if (!deps || typeof deps !== "object" || Array.isArray(deps))
2014
+ return false;
2015
+ return Object.prototype.hasOwnProperty.call(deps, packageName);
2016
+ });
2017
+ }
2018
+ /**
2019
+ * Which dependency gives this app a path to the serverless browser runtime, or
2020
+ * null when it has none. Resolution cannot answer this: findInstalledPackageRoot
2021
+ * falls back to an ancestor + .pnpm store walk, so in a workspace every app
2022
+ * "resolves" the sibling package that actually declares Chromium and ships its
2023
+ * ~80MB into every emitted function.
2024
+ */
2025
+ export function findServerlessBrowserRuntimeConsumer(projectCwd = cwd) {
2026
+ const manifest = readPackageManifest(projectCwd);
2027
+ for (const packageName of [
2028
+ ...SERVERLESS_BROWSER_RUNTIME_PACKAGES,
2029
+ SERVERLESS_BROWSER_RUNTIME_CONSUMER,
2030
+ ]) {
2031
+ if (manifestDeclaresDependency(manifest, packageName))
2032
+ return packageName;
2033
+ const linked = path.join(projectCwd, "node_modules", ...packageName.split("/"));
2034
+ if (fs.existsSync(linked))
2035
+ return packageName;
2036
+ }
2037
+ return null;
2038
+ }
1996
2039
  function packageRootFromResolvedPath(packageName, resolvedPath) {
1997
2040
  let current = path.dirname(resolvedPath);
1998
2041
  while (true) {
@@ -2062,6 +2105,16 @@ export function copyInstalledBrowserRuntimePackages(serverDir, projectCwd = cwd)
2062
2105
  if (!serverDir || !fs.existsSync(serverDir))
2063
2106
  return 0;
2064
2107
  const nodeModulesRoots = nodeModulesAncestors(projectCwd);
2108
+ const consumer = findServerlessBrowserRuntimeConsumer(projectCwd);
2109
+ if (!consumer) {
2110
+ const skippedBytes = SERVERLESS_BROWSER_RUNTIME_PACKAGES.reduce((total, packageName) => {
2111
+ const packageDir = findInstalledPackageRoot(packageName, nodeModulesRoots);
2112
+ return packageDir ? total + getDirSize(packageDir) : total;
2113
+ }, 0);
2114
+ console.log(`[deploy] Skipped the serverless browser runtime (${SERVERLESS_BROWSER_RUNTIME_PACKAGES.join(", ")}): this app depends on neither ${SERVERLESS_BROWSER_RUNTIME_CONSUMER} ` +
2115
+ `nor a browser package. Kept ${(skippedBytes / 1024 / 1024).toFixed(1)}MB out of every emitted function.`);
2116
+ return 0;
2117
+ }
2065
2118
  const copiedPackages = new Set();
2066
2119
  let copiedCount = 0;
2067
2120
  for (const packageName of SERVERLESS_BROWSER_RUNTIME_PACKAGES) {
@@ -2070,9 +2123,15 @@ export function copyInstalledBrowserRuntimePackages(serverDir, projectCwd = cwd)
2070
2123
  continue;
2071
2124
  copiedCount += copyRuntimePackageTree(packageName, packageDir, serverDir, nodeModulesRoots, copiedPackages);
2072
2125
  }
2073
- if (copiedCount > 0) {
2074
- console.log(`[deploy] Copied ${copiedCount} serverless browser runtime package(s) into the server bundle.`);
2126
+ if (copiedCount === 0) {
2127
+ // Not the same as the skip above: the app asked for the browser runtime and
2128
+ // the build could not find it, so anything that opens a browser will fail
2129
+ // at runtime instead of at build time.
2130
+ console.warn(`[deploy] ${consumer} needs the serverless browser runtime but none of ` +
2131
+ `${SERVERLESS_BROWSER_RUNTIME_PACKAGES.join(", ")} is installed; the function ships without it.`);
2132
+ return 0;
2075
2133
  }
2134
+ console.log(`[deploy] Copied ${copiedCount} serverless browser runtime package(s) into the server bundle (required by ${consumer}).`);
2076
2135
  return copiedCount;
2077
2136
  }
2078
2137
  function findInstalledLibsqlNativePackage(nodeModulesRoots, packageName) {
@@ -2847,6 +2906,60 @@ export function bundleYjsRuntimeForServerlessOutput(serverDir, projectCwd) {
2847
2906
  });
2848
2907
  return bareImports;
2849
2908
  }
2909
+ // Netlify's hard limit is 250MB unzipped per function; a bundle this far under
2910
+ // it still leaves room for growth, and today's server functions land near 70MB
2911
+ // once the dead weight is out. Apps that legitimately ship the browser runtime
2912
+ // get its ~85MB on top, so the budget stays a regression signal rather than a
2913
+ // tax on Chromium-backed templates.
2914
+ const NETLIFY_FUNCTION_SIZE_BUDGET_BYTES = 120 * 1024 * 1024;
2915
+ const NETLIFY_BROWSER_RUNTIME_SIZE_ALLOWANCE_BYTES = 100 * 1024 * 1024;
2916
+ function reportNetlifyFunctionSizes(projectCwd, internalDir, failures) {
2917
+ if (!fs.existsSync(internalDir))
2918
+ return;
2919
+ const functions = fs
2920
+ .readdirSync(internalDir, { withFileTypes: true })
2921
+ .filter((entry) => entry.isDirectory())
2922
+ .map((entry) => ({
2923
+ name: entry.name,
2924
+ dir: path.join(internalDir, entry.name),
2925
+ size: getDirSize(path.join(internalDir, entry.name)),
2926
+ }))
2927
+ .sort((a, b) => b.size - a.size);
2928
+ if (functions.length === 0)
2929
+ return;
2930
+ const toMb = (bytes) => (bytes / 1024 / 1024).toFixed(1);
2931
+ const total = functions.reduce((sum, fn) => sum + fn.size, 0);
2932
+ // Every extra function is a full second copy of the bundle: the clone is
2933
+ // hard-linked on disk, but zip-it-and-ship-it sees regular files and uploads
2934
+ // each one whole, so the total is what the deploy actually pays.
2935
+ console.log(`[deploy] Netlify functions: ${functions.length} (${functions
2936
+ .map((fn) => `${fn.name} ${toMb(fn.size)}MB`)
2937
+ .join(", ")}) — ${toMb(total)}MB uploaded in total.`);
2938
+ const budget = NETLIFY_FUNCTION_SIZE_BUDGET_BYTES +
2939
+ (findServerlessBrowserRuntimeConsumer(projectCwd)
2940
+ ? NETLIFY_BROWSER_RUNTIME_SIZE_ALLOWANCE_BYTES
2941
+ : 0);
2942
+ for (const fn of functions) {
2943
+ if (fn.size <= budget)
2944
+ continue;
2945
+ // node_modules is always the biggest child and names nothing useful; the
2946
+ // packages inside it are what a regression actually adds.
2947
+ const nodeModulesDir = path.join(fn.dir, "node_modules");
2948
+ const inspectDir = fs.existsSync(nodeModulesDir) ? nodeModulesDir : fn.dir;
2949
+ const largest = fs
2950
+ .readdirSync(inspectDir, { withFileTypes: true })
2951
+ .filter((entry) => entry.isDirectory())
2952
+ .map((entry) => ({
2953
+ name: entry.name,
2954
+ size: getDirSize(path.join(inspectDir, entry.name)),
2955
+ }))
2956
+ .sort((a, b) => b.size - a.size)
2957
+ .slice(0, 3)
2958
+ .map((child) => `${child.name} ${toMb(child.size)}MB`)
2959
+ .join(", ");
2960
+ failures.push(`function ${fn.name} is ${toMb(fn.size)}MB, over the ${toMb(budget)}MB budget — largest: ${largest}`);
2961
+ }
2962
+ }
2850
2963
  export function assertSingleTemplateNetlifyBuildOutput(projectCwd) {
2851
2964
  const failures = [];
2852
2965
  const publishDir = path.join(projectCwd, "dist");
@@ -2988,9 +3101,10 @@ export function assertSingleTemplateNetlifyBuildOutput(projectCwd) {
2988
3101
  }
2989
3102
  }
2990
3103
  }
3104
+ reportNetlifyFunctionSizes(projectCwd, internalDir, failures);
2991
3105
  if (failures.length > 0) {
2992
3106
  throw new Error("[deploy] Netlify deploy guard failed; refusing to publish an output " +
2993
- "that would likely serve Netlify 404s:\n" +
3107
+ "that would likely serve Netlify 404s or blow the function size limit:\n" +
2994
3108
  failures.map((failure) => `- ${failure}`).join("\n"));
2995
3109
  }
2996
3110
  console.log("[deploy] Netlify deploy guard passed: publish dir and catch-all server function are present.");
@@ -3111,13 +3225,7 @@ export function sanitizeServerlessFunctionPackageManifest(functionDir) {
3111
3225
  return;
3112
3226
  }
3113
3227
  let removed = 0;
3114
- const depFields = [
3115
- "dependencies",
3116
- "optionalDependencies",
3117
- "devDependencies",
3118
- "peerDependencies",
3119
- ];
3120
- for (const field of depFields) {
3228
+ for (const field of PACKAGE_DEPENDENCY_FIELDS) {
3121
3229
  const deps = packageJson[field];
3122
3230
  if (!deps || typeof deps !== "object" || Array.isArray(deps))
3123
3231
  continue;
@@ -3145,6 +3253,70 @@ export function sanitizeServerlessFunctionPackageManifest(functionDir) {
3145
3253
  console.log(`[deploy] Removed ${removed} desktop-only package reference(s) from ${path.relative(cwd, functionDir)}.`);
3146
3254
  }
3147
3255
  }
3256
+ /**
3257
+ * Nitro's dependency tracer is not subject to the copy-time filters above, so
3258
+ * it ships every prebuild of a multi-platform native package plus whatever the
3259
+ * local dev server left in `data/`. Prune both from the function dir before it
3260
+ * is cloned for the extra functions, since each clone is zipped and uploaded in
3261
+ * full.
3262
+ *
3263
+ * Only prunes inside a directory that still holds a runnable prebuild under the
3264
+ * `isServerlessNativePlatformPackage` naming (`@libsql`, `@resvg`). Packages
3265
+ * that name prebuilds differently — `@img/sharp-linux-x64` has no gnu/musl
3266
+ * suffix — read as entirely dead and must be left alone.
3267
+ */
3268
+ export function pruneServerlessFunctionDeadWeight(functionDir) {
3269
+ if (!functionDir || !fs.existsSync(functionDir))
3270
+ return 0;
3271
+ let removedBytes = 0;
3272
+ const removedNames = [];
3273
+ const nodeModulesDir = path.join(functionDir, "node_modules");
3274
+ const packageDirs = [];
3275
+ if (fs.existsSync(nodeModulesDir)) {
3276
+ packageDirs.push(nodeModulesDir);
3277
+ for (const entry of fs.readdirSync(nodeModulesDir, {
3278
+ withFileTypes: true,
3279
+ })) {
3280
+ if (entry.isDirectory() && entry.name.startsWith("@")) {
3281
+ packageDirs.push(path.join(nodeModulesDir, entry.name));
3282
+ }
3283
+ }
3284
+ }
3285
+ for (const packageDir of packageDirs) {
3286
+ const prebuilds = fs
3287
+ .readdirSync(packageDir, { withFileTypes: true })
3288
+ .filter((entry) => entry.isDirectory() &&
3289
+ SERVERLESS_PLATFORM_PACKAGE_NAME.test(entry.name))
3290
+ .map((entry) => entry.name);
3291
+ const runnable = prebuilds.filter(isServerlessNativePlatformPackage);
3292
+ if (runnable.length === 0)
3293
+ continue;
3294
+ for (const name of prebuilds) {
3295
+ if (isServerlessNativePlatformPackage(name))
3296
+ continue;
3297
+ const deadDir = path.join(packageDir, name);
3298
+ removedBytes += getDirSize(deadDir);
3299
+ removedNames.push(path.relative(functionDir, deadDir));
3300
+ fs.rmSync(deadDir, { recursive: true, force: true });
3301
+ }
3302
+ const survivors = runnable.filter((name) => fs.existsSync(path.join(packageDir, name)));
3303
+ if (survivors.length === 0) {
3304
+ throw new Error(`[deploy] Pruning dead-platform prebuilds from ${path.relative(cwd, packageDir)} removed every runnable Linux prebuild (had: ${runnable.join(", ")}).`);
3305
+ }
3306
+ }
3307
+ const dataDir = path.join(functionDir, "data");
3308
+ if (fs.existsSync(dataDir)) {
3309
+ removedBytes += getDirSize(dataDir);
3310
+ removedNames.push("data");
3311
+ fs.rmSync(dataDir, { recursive: true, force: true });
3312
+ }
3313
+ if (removedNames.length > 0) {
3314
+ console.log(`[deploy] Pruned ${removedNames.length} unrunnable path(s) (${(removedBytes /
3315
+ 1024 /
3316
+ 1024).toFixed(1)}MB) from ${path.relative(cwd, functionDir)}: ${removedNames.join(", ")}.`);
3317
+ }
3318
+ return removedBytes;
3319
+ }
3148
3320
  /**
3149
3321
  * Create stub directories for dangling platform-specific optional dependency
3150
3322
  * symlinks in the pnpm store.
@@ -3612,6 +3784,9 @@ export default bundle;
3612
3784
  copyInstalledFfmpegStaticPackage(nitro.options.output.serverDir);
3613
3785
  copyInstalledBrowserRuntimePackages(nitro.options.output.serverDir);
3614
3786
  sanitizeServerlessFunctionPackageManifest(nitro.options.output.serverDir);
3787
+ // Before the Netlify block below clones this dir into the extra functions,
3788
+ // so they inherit the pruned bundle instead of a second full copy.
3789
+ pruneServerlessFunctionDeadWeight(nitro.options.output.serverDir);
3615
3790
  bundleYjsRuntimeForServerlessOutput(nitro.options.output.serverDir, cwd);
3616
3791
  }
3617
3792
  if (isCloudflareModulePreset(preset)) {
@@ -12,7 +12,7 @@ export interface BackgroundAutomationContext {
12
12
  resource: Resource;
13
13
  }
14
14
  export interface BackgroundAutomationDeps {
15
- getActions: (automation?: BackgroundAutomationContext) => Record<string, ActionEntry>;
15
+ getActions: (automation?: BackgroundAutomationContext) => Record<string, ActionEntry> | Promise<Record<string, ActionEntry>>;
16
16
  getSystemPrompt: (owner: string) => Promise<string>;
17
17
  getInitialToolNames?: (automation?: BackgroundAutomationContext) => string[] | undefined;
18
18
  engine?: AgentEngine;
@@ -221,7 +221,7 @@ async function executeBackgroundAutomation(options, deps, historyId) {
221
221
  userEmail: ownerEmail,
222
222
  orgId,
223
223
  }, async () => {
224
- const baseActions = deps.getActions(automation);
224
+ const baseActions = await deps.getActions(automation);
225
225
  assertRequestedMcpToolsAvailable(automation, baseActions);
226
226
  const configuredInitialTools = deps.getInitialToolNames?.(automation);
227
227
  const initialToolNames = configuredInitialTools
@@ -1,3 +1,7 @@
1
+ import { type MigrationEntry } from "../db/migrations.js";
2
+ /** Authoritative release-time schema for recurring scheduler health. */
3
+ export declare const AUTOMATION_SCHEDULER_HEALTH_MIGRATIONS: MigrationEntry[];
4
+ export declare function runAutomationSchedulerHealthMigrations(nitroApp: unknown): Promise<void>;
1
5
  export interface AutomationSchedulerHealth {
2
6
  appId: string;
3
7
  orgId: string | null;
@@ -1,8 +1,41 @@
1
1
  import { getDbExec, intType, isPostgres } from "../db/client.js";
2
2
  import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db/ddl-guard.js";
3
+ import { runMigrations } from "../db/migrations.js";
3
4
  const TABLE = "automation_scheduler_health";
4
5
  const DEFAULT_APP_ID = "default";
5
6
  const MAX_ERROR_LENGTH = 500;
7
+ /** Authoritative release-time schema for recurring scheduler health. */
8
+ export const AUTOMATION_SCHEDULER_HEALTH_MIGRATIONS = [
9
+ {
10
+ version: 1,
11
+ name: "automation-scheduler-health-table",
12
+ sql: `
13
+ CREATE TABLE IF NOT EXISTS ${TABLE} (
14
+ id TEXT PRIMARY KEY,
15
+ app_id TEXT NOT NULL DEFAULT 'default',
16
+ org_id TEXT,
17
+ last_checked_at INTEGER,
18
+ last_dispatched_at INTEGER,
19
+ last_error TEXT,
20
+ runtime TEXT,
21
+ updated_at INTEGER NOT NULL DEFAULT 0
22
+ );
23
+ ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS app_id TEXT NOT NULL DEFAULT 'default';
24
+ ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS org_id TEXT;
25
+ ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS last_checked_at INTEGER;
26
+ ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS last_dispatched_at INTEGER;
27
+ ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS last_error TEXT;
28
+ ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS runtime TEXT;
29
+ ALTER TABLE ${TABLE} ADD COLUMN IF NOT EXISTS updated_at INTEGER NOT NULL DEFAULT 0;
30
+ CREATE INDEX IF NOT EXISTS idx_${TABLE}_updated ON ${TABLE} (updated_at)
31
+ `,
32
+ },
33
+ ];
34
+ export async function runAutomationSchedulerHealthMigrations(nitroApp) {
35
+ await runMigrations(AUTOMATION_SCHEDULER_HEALTH_MIGRATIONS, {
36
+ table: "_automation_scheduler_health_migrations",
37
+ })(nitroApp);
38
+ }
6
39
  function normalizeAppId(appId) {
7
40
  const normalized = appId?.trim();
8
41
  return normalized || DEFAULT_APP_ID;
@@ -16,7 +16,6 @@ export interface ResolveLocaleFromRequestOptions {
16
16
  export interface LocaleInitScriptOptions {
17
17
  locale?: LocaleCode;
18
18
  preference?: LocalizationPreference | LocalePreference;
19
- messages?: Record<string, unknown> | null;
20
19
  }
21
20
  export declare function parseAcceptLanguage(header: string | null | undefined): string[];
22
21
  export declare function resolveLocaleFromRequest(options?: ResolveLocaleFromRequestOptions): ResolvedRequestLocale;
@@ -58,11 +58,13 @@ export function getLocaleInitScript(options = {}) {
58
58
  const safeLocale = normalizeLocaleCode(options.locale) ?? DEFAULT_LOCALE;
59
59
  const shouldStorePreference = options.preference !== undefined;
60
60
  const safePreference = normalizeLocalizationPreference(options.preference ?? { locale: "system" });
61
+ // Translations are deliberately absent here: this script is render-blocking in
62
+ // <head>, and the provider already receives the same catalog through loader
63
+ // data as `initialMessages`. Adding them back doubles every localized payload.
61
64
  const payload = {
62
65
  locale: safeLocale,
63
66
  preference: safePreference,
64
67
  dir: localeDirection(safeLocale),
65
- messages: options.messages ?? undefined,
66
68
  };
67
69
  return `(function(){try{var supported=${JSON.stringify(SUPPORTED_LOCALES)};var payload=${JSON.stringify(payload)};var shouldStorePreference=${JSON.stringify(shouldStorePreference)};function valid(x){return supported.indexOf(x)>=0}function canon(x){if(typeof x!=='string'||!x)return null;try{var c=Intl.getCanonicalLocales(x)[0];if(valid(c))return c;var lang=c&&c.split('-')[0].toLowerCase();for(var i=0;i<supported.length;i++){if(supported[i].split('-')[0].toLowerCase()===lang)return supported[i]}}catch(e){}return null}function storageGet(k){try{return window.localStorage.getItem(k)}catch(e){return null}}function storageSet(k,v){try{window.localStorage.setItem(k,v)}catch(e){}}var stored=storageGet(${JSON.stringify(LOCALE_STORAGE_KEY)});var pref=payload.preference&&payload.preference.locale;var locale=payload.locale;if(!valid(locale)){locale=null}if(!locale&&stored&&stored!=='system'){locale=canon(stored)}if(!locale&&pref&&pref!=='system'){locale=canon(pref)}if(!locale){var langs=navigator.languages&&navigator.languages.length?navigator.languages:[navigator.language];for(var j=0;j<langs.length&&!locale;j++){locale=canon(langs[j])}}if(!locale)locale=${JSON.stringify(DEFAULT_LOCALE)};var root=document.documentElement;root.setAttribute('lang',locale);root.setAttribute('dir',locale==='ar-SA'?'rtl':'ltr');root.setAttribute('data-locale',locale);payload.locale=locale;payload.dir=locale==='ar-SA'?'rtl':'ltr';window[${JSON.stringify(LOCALE_HYDRATION_GLOBAL)}]=payload;if(shouldStorePreference&&pref){storageSet(${JSON.stringify(LOCALE_STORAGE_KEY)},pref)}}catch(e){}})();`;
68
70
  }
@@ -10,7 +10,7 @@ export { listRemoteServers, addRemoteServer, addOAuthRemoteServer, addFirstParty
10
10
  export { finishMcpOAuthAuthorization, getMcpOAuthAccessToken, readMcpOAuthCredentials, saveMcpOAuthCredentials, startMcpOAuthAuthorization, type McpOAuthCallbackResult, type McpOAuthCredentialBundle, type McpOAuthDiscoveryState, type McpOAuthProviderOptions, type McpOAuthStartResult, } from "./oauth-client.js";
11
11
  export { areBuiltinMcpCapabilitiesSupported, BUILTIN_MCP_CAPABILITIES, getBuiltinMcpCapability, isBuiltinMcpCapabilityAvailable, listSupportedBuiltinMcpCapabilities, normalizeBuiltinMcpCapabilityIds, toBuiltinMcpServerConfig, type BuiltinMcpCapability, type BuiltinMcpCapabilityId, } from "./builtin-capabilities.js";
12
12
  export { builtinMcpCapabilitiesSettingsKey, listEnabledBuiltinMcpCapabilities, setEnabledBuiltinMcpCapabilities, setBuiltinMcpCapabilityEnabled, type StoredBuiltinMcpCapabilities, } from "./builtin-store.js";
13
- export { mountMcpServersRoutes, buildMergedConfig, builtinMergedConfigKey, startMcpConfigRefresh, type ClientBuiltinCapability, } from "./routes.js";
13
+ export { mountMcpServersRoutes, buildMergedConfig, builtinMergedConfigKey, startMcpConfigRefresh, McpConfigUnreadableError, type ClientBuiltinCapability, } from "./routes.js";
14
14
  export { mountMcpHubRoutes, listHubServers, getHubStatus, isHubServeEnabled, isHubConsumeEnabled, type HubServerRecord, type HubServersResponse, } from "./hub-routes.js";
15
15
  export { fetchHubServers } from "./hub-client.js";
16
16
  export { isMcpToolAllowedForRequest } from "./visibility.js";
@@ -10,7 +10,7 @@ export { listRemoteServers, addRemoteServer, addOAuthRemoteServer, addFirstParty
10
10
  export { finishMcpOAuthAuthorization, getMcpOAuthAccessToken, readMcpOAuthCredentials, saveMcpOAuthCredentials, startMcpOAuthAuthorization, } from "./oauth-client.js";
11
11
  export { areBuiltinMcpCapabilitiesSupported, BUILTIN_MCP_CAPABILITIES, getBuiltinMcpCapability, isBuiltinMcpCapabilityAvailable, listSupportedBuiltinMcpCapabilities, normalizeBuiltinMcpCapabilityIds, toBuiltinMcpServerConfig, } from "./builtin-capabilities.js";
12
12
  export { builtinMcpCapabilitiesSettingsKey, listEnabledBuiltinMcpCapabilities, setEnabledBuiltinMcpCapabilities, setBuiltinMcpCapabilityEnabled, } from "./builtin-store.js";
13
- export { mountMcpServersRoutes, buildMergedConfig, builtinMergedConfigKey, startMcpConfigRefresh, } from "./routes.js";
13
+ export { mountMcpServersRoutes, buildMergedConfig, builtinMergedConfigKey, startMcpConfigRefresh, McpConfigUnreadableError, } from "./routes.js";
14
14
  export { mountMcpHubRoutes, listHubServers, getHubStatus, isHubServeEnabled, isHubConsumeEnabled, } from "./hub-routes.js";
15
15
  export { fetchHubServers } from "./hub-client.js";
16
16
  export { isMcpToolAllowedForRequest } from "./visibility.js";