@agent-native/core 0.141.2 → 0.141.4

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 (157) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/analysis-workspace/SKILL.md +1 -1
  3. package/corpus/templates/analytics/actions/gong-calls.ts +56 -18
  4. package/corpus/templates/analytics/changelog/2026-08-05-analytics-queries-keep-every-table-tenant-scoped.md +6 -0
  5. package/corpus/templates/analytics/changelog/2026-08-05-durable-a2a-workers-avoid-startup-migration-contention.md +6 -0
  6. package/corpus/templates/analytics/changelog/2026-08-05-fixed-sign-in-and-dashboard-requests-failing-when-background.md +6 -0
  7. package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +6 -0
  8. package/corpus/templates/analytics/server/db/schema.ts +1 -0
  9. package/corpus/templates/analytics/server/lib/dashboard-report.ts +2 -0
  10. package/corpus/templates/analytics/server/lib/emails.ts +51 -0
  11. package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +8 -12
  12. package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -0
  13. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +262 -13
  14. package/corpus/templates/analytics/server/lib/gong.ts +132 -33
  15. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
  16. package/corpus/templates/analytics/server/plugins/auth.ts +21 -2
  17. package/corpus/templates/analytics/server/plugins/db.ts +35 -10
  18. package/corpus/templates/analytics/server/plugins/transactional-emails.ts +5 -0
  19. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +17 -7
  20. package/corpus/templates/calendar/server/lib/booking-emails.ts +154 -70
  21. package/corpus/templates/calendar/server/lib/emails.ts +167 -0
  22. package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +64 -28
  23. package/corpus/templates/calendar/server/plugins/transactional-emails.ts +5 -0
  24. package/corpus/templates/clips/actions/invite-member.ts +39 -16
  25. package/corpus/templates/clips/app/components/player/share-dialog.tsx +73 -1
  26. package/corpus/templates/clips/app/i18n/en-US.ts +3 -0
  27. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -25
  28. package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
  29. package/corpus/templates/clips/changelog/2026-08-05-activity-is-now-the-default-clips-recording-tab-and-ai-setup.md +6 -0
  30. package/corpus/templates/clips/changelog/2026-08-05-clips-can-copy-a-linked-email-preview-for-public-recordings.md +6 -0
  31. package/corpus/templates/clips/changelog/2026-08-05-preparing-recording-now-uses-a-clean-shadow-free-status-card.md +6 -0
  32. package/corpus/templates/clips/desktop/src/styles.css +0 -3
  33. package/corpus/templates/clips/server/lib/emails.ts +256 -0
  34. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +24 -0
  35. package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +5 -0
  36. package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +4 -1
  37. package/corpus/templates/clips/shared/email-preview.ts +61 -0
  38. package/corpus/templates/content/server/lib/comment-notifications.ts +57 -26
  39. package/corpus/templates/content/server/lib/emails.ts +70 -0
  40. package/corpus/templates/content/server/plugins/transactional-emails.ts +5 -0
  41. package/corpus/templates/design/actions/import-design-tokens.ts +11 -8
  42. package/corpus/templates/design/actions/index-design-tokens.ts +13 -6
  43. package/corpus/templates/design/changelog/2026-08-05-design-token-imports-classify-typography-dimensions-correctly.md +6 -0
  44. package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +4 -0
  45. package/corpus/templates/dispatch/app/routes/transactional-email.tsx +4 -0
  46. package/corpus/templates/dispatch/changelog/2026-08-05-transactional-email-catalogs-and-delivery-activity-are-available-again.md +6 -0
  47. package/corpus/templates/factory/AGENTS.md +18 -17
  48. package/corpus/templates/factory/actions/get-factory-automation-health.ts +50 -0
  49. package/corpus/templates/factory/actions/get-triage-config.ts +8 -0
  50. package/corpus/templates/factory/actions/save-triage-config.ts +26 -0
  51. package/corpus/templates/factory/app/i18n/en-US.ts +27 -0
  52. package/corpus/templates/factory/app/routes/factory.tsx +376 -206
  53. package/corpus/templates/factory/changelog/2026-08-05-factory-now-keeps-production-automations-running-shows-sched.md +6 -0
  54. package/corpus/templates/factory/server/db/schema.ts +6 -0
  55. package/corpus/templates/factory/server/plugins/factory-migrations.ts +10 -0
  56. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +141 -2
  57. package/corpus/templates/forms/app/pages/FormsListPage.tsx +41 -21
  58. package/corpus/templates/forms/changelog/2026-08-05-form-links-can-now-be-cmd-clicked-to-open-in-a-new-tab.md +6 -0
  59. package/corpus/templates/forms/server/lib/emails.ts +52 -0
  60. package/corpus/templates/forms/server/lib/response-email.ts +2 -0
  61. package/corpus/templates/forms/server/plugins/transactional-emails.ts +5 -0
  62. package/corpus/templates/plan/actions/request-plan-access.ts +33 -12
  63. package/corpus/templates/plan/server/lib/comment-notifications.ts +53 -27
  64. package/corpus/templates/plan/server/lib/emails.ts +67 -0
  65. package/corpus/templates/plan/server/plugins/transactional-emails.ts +5 -0
  66. package/corpus/templates/slides/actions/import-from-url.ts +1 -0
  67. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +492 -381
  68. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +8 -7
  69. package/corpus/templates/slides/app/components/layout/Layout.tsx +27 -23
  70. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +313 -0
  71. package/corpus/templates/slides/app/components/ui/command.tsx +1 -0
  72. package/corpus/templates/slides/app/i18n/en-US.ts +8 -2
  73. package/corpus/templates/slides/app/lib/create-deck-generation.ts +305 -0
  74. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -12
  75. package/corpus/templates/slides/app/pages/Index.tsx +127 -137
  76. package/corpus/templates/slides/app/root.tsx +8 -0
  77. package/corpus/templates/slides/changelog/2026-08-05-deck-browsing-keeps-all-and-mine-in-the-header-with-a-tighte.md +6 -0
  78. package/corpus/templates/slides/changelog/2026-08-05-new-deck-creation-now-flows-through-a-clean-full-screen-refe.md +6 -0
  79. package/corpus/templates/slides/changelog/2026-08-05-website-imports-now-include-linked-stylesheets-for-more-accu.md +6 -0
  80. package/corpus/templates/slides/server/lib/comment-notifications.ts +44 -21
  81. package/corpus/templates/slides/server/lib/emails.ts +40 -0
  82. package/corpus/templates/slides/server/plugins/transactional-emails.ts +5 -0
  83. package/dist/brand-kit/index.d.ts +1 -0
  84. package/dist/brand-kit/index.js +1 -0
  85. package/dist/brand-kit/tokens.d.ts +91 -0
  86. package/dist/brand-kit/tokens.js +279 -0
  87. package/dist/brand-kit/types.d.ts +25 -0
  88. package/dist/client/AssistantChat.js +7 -7
  89. package/dist/client/onboarding/FirstRunOnboarding.js +26 -2
  90. package/dist/client/onboarding/first-run-registry.d.ts +25 -0
  91. package/dist/client/onboarding/first-run-registry.js +18 -0
  92. package/dist/client/onboarding/index.d.ts +2 -0
  93. package/dist/client/onboarding/index.js +1 -0
  94. package/dist/collab/awareness.d.ts +2 -2
  95. package/dist/collab/routes.d.ts +1 -1
  96. package/dist/collab/struct-routes.d.ts +1 -1
  97. package/dist/db/index.d.ts +1 -1
  98. package/dist/db/index.js +1 -1
  99. package/dist/db/migrations.d.ts +13 -3
  100. package/dist/db/migrations.js +19 -4
  101. package/dist/deploy/build.d.ts +8 -0
  102. package/dist/deploy/build.js +92 -3
  103. package/dist/deploy/workspace-deploy.js +86 -3
  104. package/dist/email-catalog/actions/list-email-activity.d.ts +5 -0
  105. package/dist/email-catalog/actions/list-email-activity.js +19 -0
  106. package/dist/email-catalog/actions/list-email-engagement.d.ts +5 -0
  107. package/dist/email-catalog/actions/list-email-engagement.js +15 -0
  108. package/dist/email-catalog/actions/list-email-log.d.ts +7 -0
  109. package/dist/email-catalog/actions/list-email-log.js +23 -0
  110. package/dist/email-catalog/actions/list-transactional-emails.d.ts +22 -0
  111. package/dist/email-catalog/actions/list-transactional-emails.js +67 -0
  112. package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +9 -0
  113. package/dist/email-catalog/actions/render-transactional-email-preview.js +25 -0
  114. package/dist/email-catalog/authorize.d.ts +1 -0
  115. package/dist/email-catalog/authorize.js +9 -0
  116. package/dist/email-catalog/log.d.ts +60 -0
  117. package/dist/email-catalog/log.js +142 -0
  118. package/dist/email-catalog/provider-metrics.d.ts +34 -0
  119. package/dist/email-catalog/provider-metrics.js +182 -0
  120. package/dist/email-catalog/registry.d.ts +78 -0
  121. package/dist/email-catalog/registry.js +70 -0
  122. package/dist/email-catalog/schema.d.ts +231 -0
  123. package/dist/email-catalog/schema.js +53 -0
  124. package/dist/email-catalog/system-emails.d.ts +13 -0
  125. package/dist/email-catalog/system-emails.js +67 -0
  126. package/dist/jobs/index.d.ts +2 -0
  127. package/dist/jobs/index.js +2 -0
  128. package/dist/jobs/run-history.js +48 -1
  129. package/dist/jobs/scheduler-dispatch.d.ts +7 -0
  130. package/dist/jobs/scheduler-dispatch.js +7 -0
  131. package/dist/jobs/scheduler-health.d.ts +22 -0
  132. package/dist/jobs/scheduler-health.js +172 -0
  133. package/dist/jobs/scheduler.js +67 -2
  134. package/dist/localization/default-messages.d.ts +45 -0
  135. package/dist/localization/default-messages.js +45 -0
  136. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  137. package/dist/notifications/routes.d.ts +3 -3
  138. package/dist/observability/routes.d.ts +3 -3
  139. package/dist/org/handlers.js +9 -1
  140. package/dist/progress/routes.d.ts +1 -1
  141. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  142. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  143. package/dist/server/action-discovery.js +22 -0
  144. package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +7 -1
  145. package/dist/server/agent-chat/recurring-jobs-runtime.js +12 -0
  146. package/dist/server/agent-chat-plugin.d.ts +2 -1
  147. package/dist/server/agent-chat-plugin.js +69 -22
  148. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  149. package/dist/server/better-auth-instance.js +17 -2
  150. package/dist/server/design-token-utils.d.ts +6 -1
  151. package/dist/server/design-token-utils.js +252 -59
  152. package/dist/server/email.d.ts +11 -0
  153. package/dist/server/email.js +54 -3
  154. package/dist/server/index.d.ts +3 -2
  155. package/dist/server/index.js +3 -2
  156. package/dist/vite/client.js +20 -14
  157. package/package.json +2 -1
@@ -154,6 +154,15 @@ function splitSqlStatements(sql) {
154
154
  out.push(tail);
155
155
  return out;
156
156
  }
157
+ /**
158
+ * A run-only migration can defer its work without recording itself as applied.
159
+ * This is for serialized backfills where another instance owns the advisory
160
+ * lock; throwing would turn an expected loser into a startup retry storm.
161
+ */
162
+ export const MIGRATION_DEFERRED = Symbol("migration-deferred");
163
+ export function deferMigration() {
164
+ return MIGRATION_DEFERRED;
165
+ }
157
166
  function resolveMigrationSql(sql, pg) {
158
167
  if (typeof sql === "string")
159
168
  return sql;
@@ -272,8 +281,11 @@ export function runMigrations(migrations, options) {
272
281
  try {
273
282
  // D1 is SQLite-compatible
274
283
  const raw = resolveMigrationSql(m.sql, false);
275
- if (m.run)
276
- await m.run();
284
+ const runResult = m.run ? await m.run() : undefined;
285
+ if (runResult === MIGRATION_DEFERRED) {
286
+ console.info(`[db] Deferred migration ${m.name ? `"${m.name}" ` : ""}v${m.version}; it remains pending for a later boot`);
287
+ continue;
288
+ }
277
289
  const recordStatements = [
278
290
  m.name
279
291
  ? d1
@@ -482,8 +494,11 @@ export function runMigrations(migrations, options) {
482
494
  // A throw here escapes to the outer handler with nothing recorded,
483
495
  // so the entry is retried on the next boot rather than being marked
484
496
  // applied against work that never happened.
485
- if (m.run)
486
- await m.run();
497
+ const runResult = m.run ? await m.run() : undefined;
498
+ if (runResult === MIGRATION_DEFERRED) {
499
+ console.info(`[db] Deferred migration ${label}; it remains pending for a later boot`);
500
+ continue;
501
+ }
487
502
  if (raw == null) {
488
503
  // Dialect-gated migration with no SQL for this dialect; still mark
489
504
  // as applied so we don't retry forever.
@@ -90,6 +90,8 @@ export declare function findInstalledResvgPackages(nodeModulesRoots: string[]):
90
90
  export declare function isDurableBackgroundDeployEnabled(): boolean;
91
91
  export declare const NETLIFY_INTEGRATION_RECOVERY_FUNCTION_NAME = "server-integration-recovery";
92
92
  export declare function isIntegrationDurableDispatchDeployEnabled(): boolean;
93
+ export declare const NETLIFY_RECURRING_JOBS_FUNCTION_NAME = "agent-native-recurring-jobs";
94
+ export declare function isRecurringJobsDeployEnabled(): boolean;
93
95
  /**
94
96
  * Emit a site-local Netlify Scheduled Function that wakes the public server
95
97
  * function and its database every minute. GitHub Actions schedules can be
@@ -97,6 +99,12 @@ export declare function isIntegrationDurableDispatchDeployEnabled(): boolean;
97
99
  * autosuspend window and leaves the next visitor to pay the cold-start cost.
98
100
  */
99
101
  export declare function emitSingleTemplateNetlifyKeepWarmFunction(projectCwd: string): void;
102
+ /**
103
+ * Emit the durable recurring-job trigger. Netlify's scheduled function only
104
+ * hands off work; the existing `-background` function owns the long sweep so
105
+ * a model run is not constrained by the synchronous scheduled-function wall.
106
+ */
107
+ export declare function emitSingleTemplateNetlifyRecurringJobsFunction(projectCwd: string): void;
100
108
  /**
101
109
  * Single-template Netlify build: emit an async (background) function INSIDE the
102
110
  * scanned functions dir so the chat `_process-run` worker runs on Netlify's
@@ -20,6 +20,7 @@ import path from "path";
20
20
  import { fileURLToPath } from "url";
21
21
  import { AGENT_BACKGROUND_FUNCTION_NAME, AGENT_BACKGROUND_FUNCTION_URL_PATH, AGENT_BACKGROUND_PROCESSOR_A2A, AGENT_BACKGROUND_PROCESSOR_FIELD, AGENT_BACKGROUND_PROCESSOR_INTEGRATION, AGENT_BACKGROUND_PROCESSOR_ROUTE, AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD, AGENT_CHAT_PROCESS_RUN_PATH, isDurableBackgroundFlagExplicitlyDisabled, } from "../agent/durable-background.js";
22
22
  import { INTEGRATION_RECOVERY_RUNTIME_MARKER, INTEGRATION_RETRY_SWEEP_PATH, INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT, isIntegrationDurableDispatchConfigured, } from "../integrations/integration-durable-dispatch-config.js";
23
+ import { RECURRING_JOBS_SWEEP_PATH, RECURRING_JOBS_SWEEP_TOKEN_SUBJECT, } from "../jobs/scheduler-dispatch.js";
23
24
  import { normalizeAppBasePath } from "../server/app-base-path.js";
24
25
  import { DEFAULT_SPECULATION_RULES_PATH, resolveSsrCacheHeaders, } from "../shared/cache-control.js";
25
26
  import { mcpEmbedStaticAssetRouteRules } from "../shared/mcp-embed-headers.js";
@@ -2090,13 +2091,19 @@ export function isDurableBackgroundDeployEnabled() {
2090
2091
  */
2091
2092
  function isDurableBackgroundEmitRequired() {
2092
2093
  return (isDurableBackgroundDeployEnabled() ||
2093
- isIntegrationDurableDispatchDeployEnabled());
2094
+ isIntegrationDurableDispatchDeployEnabled() ||
2095
+ isRecurringJobsDeployEnabled());
2094
2096
  }
2095
2097
  export const NETLIFY_INTEGRATION_RECOVERY_FUNCTION_NAME = "server-integration-recovery";
2096
2098
  export function isIntegrationDurableDispatchDeployEnabled() {
2097
2099
  return isIntegrationDurableDispatchConfigured();
2098
2100
  }
2099
2101
  const NETLIFY_KEEP_WARM_FUNCTION_NAME = "agent-native-keep-warm";
2102
+ export const NETLIFY_RECURRING_JOBS_FUNCTION_NAME = "agent-native-recurring-jobs";
2103
+ export function isRecurringJobsDeployEnabled() {
2104
+ const value = process.env.AGENT_NATIVE_DISABLE_RECURRING_JOBS?.trim();
2105
+ return !value || !["1", "true", "yes", "on"].includes(value.toLowerCase());
2106
+ }
2100
2107
  /**
2101
2108
  * Emit a site-local Netlify Scheduled Function that wakes the public server
2102
2109
  * function and its database every minute. GitHub Actions schedules can be
@@ -2170,7 +2177,7 @@ export default async function handler(request) {
2170
2177
  }
2171
2178
 
2172
2179
  if (!response.ok) {
2173
- const body = await response.text().catch(() => "");
2180
+ const body = await response.text();
2174
2181
  throw new Error(
2175
2182
  "[agent-native-keep-warm] Health request failed with " +
2176
2183
  response.status +
@@ -2194,6 +2201,84 @@ export const config = {
2194
2201
  fs.writeFileSync(path.join(dest, `${NETLIFY_KEEP_WARM_FUNCTION_NAME}.mjs`), entry);
2195
2202
  console.log(`[build] Emitted Netlify scheduled keep-warm function "${NETLIFY_KEEP_WARM_FUNCTION_NAME}".`);
2196
2203
  }
2204
+ /**
2205
+ * Emit the durable recurring-job trigger. Netlify's scheduled function only
2206
+ * hands off work; the existing `-background` function owns the long sweep so
2207
+ * a model run is not constrained by the synchronous scheduled-function wall.
2208
+ */
2209
+ export function emitSingleTemplateNetlifyRecurringJobsFunction(projectCwd) {
2210
+ if (!isRecurringJobsDeployEnabled())
2211
+ return;
2212
+ const internalDir = path.join(projectCwd, ".netlify", "functions-internal");
2213
+ const backgroundEntry = path.join(internalDir, AGENT_BACKGROUND_FUNCTION_NAME, `${AGENT_BACKGROUND_FUNCTION_NAME}.mjs`);
2214
+ if (!fs.existsSync(backgroundEntry)) {
2215
+ throw new Error("[build] Recurring-job trigger cannot be emitted without the durable background function.");
2216
+ }
2217
+ const functionName = NETLIFY_RECURRING_JOBS_FUNCTION_NAME;
2218
+ const dest = path.join(internalDir, functionName);
2219
+ fs.rmSync(dest, { recursive: true, force: true });
2220
+ fs.mkdirSync(dest, { recursive: true });
2221
+ const entry = `import { createHmac } from "node:crypto";
2222
+
2223
+ const BACKGROUND_PATH = ${JSON.stringify(AGENT_BACKGROUND_FUNCTION_URL_PATH)};
2224
+ const SWEEP_PATH = ${JSON.stringify(RECURRING_JOBS_SWEEP_PATH)};
2225
+ const TOKEN_SUBJECT = ${JSON.stringify(RECURRING_JOBS_SWEEP_TOKEN_SUBJECT)};
2226
+ const PROCESSOR_FIELD = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_FIELD)};
2227
+ const PROCESSOR_ROUTE = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE)};
2228
+ const PROCESSOR_ROUTE_FIELD = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD)};
2229
+
2230
+ function siteOrigin(request) {
2231
+ const configured = process.env.URL || process.env.DEPLOY_URL;
2232
+ if (configured) return configured;
2233
+ return new URL(request.url).origin;
2234
+ }
2235
+
2236
+ function token(secret) {
2237
+ const timestamp = Date.now();
2238
+ const signature = createHmac("sha256", secret)
2239
+ .update(\`\${TOKEN_SUBJECT}:\${timestamp}\`)
2240
+ .digest("hex");
2241
+ return \`\${timestamp}.\${signature}\`;
2242
+ }
2243
+
2244
+ export default async function handler(request) {
2245
+ const secret = process.env.A2A_SECRET;
2246
+ if (!secret) {
2247
+ throw new Error("[recurring-jobs] A2A_SECRET is required for the scheduled sweep");
2248
+ }
2249
+ const url = new URL(BACKGROUND_PATH, siteOrigin(request));
2250
+ const response = await fetch(url, {
2251
+ method: "POST",
2252
+ headers: {
2253
+ Authorization: \`Bearer \${token(secret)}\`,
2254
+ "Content-Type": "application/json",
2255
+ "user-agent": "agent-native-recurring-jobs",
2256
+ },
2257
+ body: JSON.stringify({
2258
+ [PROCESSOR_FIELD]: PROCESSOR_ROUTE,
2259
+ [PROCESSOR_ROUTE_FIELD]: SWEEP_PATH,
2260
+ }),
2261
+ });
2262
+ if (!response.ok) {
2263
+ const body = await response.text();
2264
+ throw new Error(
2265
+ \`[recurring-jobs] Durable sweep handoff failed (\${response.status}): \${body.slice(0, 500)}\`,
2266
+ );
2267
+ }
2268
+ console.log("[recurring-jobs] Durable sweep handed off", url.toString());
2269
+ return new Response(null, { status: 204 });
2270
+ }
2271
+
2272
+ export const config = {
2273
+ name: "agent-native recurring jobs",
2274
+ generator: "agent-native build",
2275
+ schedule: "* * * * *",
2276
+ nodeBundler: "none",
2277
+ };
2278
+ `;
2279
+ fs.writeFileSync(path.join(dest, `${functionName}.mjs`), entry);
2280
+ console.log(`[build] Emitted Netlify scheduled recurring-job function "${functionName}".`);
2281
+ }
2197
2282
  /**
2198
2283
  * Single-template Netlify build: emit an async (background) function INSIDE the
2199
2284
  * scanned functions dir so the chat `_process-run` worker runs on Netlify's
@@ -2289,6 +2374,7 @@ export function emitSingleTemplateNetlifyBackgroundFunction(projectCwd) {
2289
2374
  const backgroundProcessorIntegration = JSON.stringify(AGENT_BACKGROUND_PROCESSOR_INTEGRATION);
2290
2375
  const backgroundProcessorRoute = JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE);
2291
2376
  const backgroundProcessorRouteField = JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD);
2377
+ const recurringJobsSweepPath = JSON.stringify(RECURRING_JOBS_SWEEP_PATH);
2292
2378
  const entry = `// Mark this isolate as the durable background runtime BEFORE the handler
2293
2379
  // bundle is imported, so isInBackgroundFunctionRuntime() reliably returns true
2294
2380
  // in this function. The deployed Lambda name is NOT guaranteed to end in
@@ -2307,6 +2393,7 @@ const BACKGROUND_PROCESSOR_A2A = ${backgroundProcessorA2A};
2307
2393
  const BACKGROUND_PROCESSOR_INTEGRATION = ${backgroundProcessorIntegration};
2308
2394
  const BACKGROUND_PROCESSOR_ROUTE = ${backgroundProcessorRoute};
2309
2395
  const BACKGROUND_PROCESSOR_ROUTE_FIELD = ${backgroundProcessorRouteField};
2396
+ const RECURRING_JOBS_SWEEP_PATH = ${recurringJobsSweepPath};
2310
2397
 
2311
2398
  function processorPathFromBody(body) {
2312
2399
  if (!body) return null;
@@ -2326,7 +2413,8 @@ function processorPathFromBody(body) {
2326
2413
  parsed?.[BACKGROUND_PROCESSOR_FIELD] === BACKGROUND_PROCESSOR_ROUTE &&
2327
2414
  typeof route === "string" &&
2328
2415
  route.startsWith("/") &&
2329
- route.includes("/api/_agent-native-background/") &&
2416
+ (route === RECURRING_JOBS_SWEEP_PATH ||
2417
+ route.includes("/api/_agent-native-background/")) &&
2330
2418
  !route.includes("?") &&
2331
2419
  !route.includes("#")
2332
2420
  ) {
@@ -3367,6 +3455,7 @@ export default bundle;
3367
3455
  if (isDurableBackgroundEmitRequired()) {
3368
3456
  emitSingleTemplateNetlifyBackgroundFunction(cwd);
3369
3457
  }
3458
+ emitSingleTemplateNetlifyRecurringJobsFunction(cwd);
3370
3459
  // Emit keep-warm after the background artifact so it only pings a function
3371
3460
  // that this build actually produced.
3372
3461
  emitSingleTemplateNetlifyKeepWarmFunction(cwd);
@@ -19,10 +19,11 @@ import fs from "fs";
19
19
  import path from "path";
20
20
  import { AGENT_BACKGROUND_PROCESSOR_A2A, AGENT_BACKGROUND_PROCESSOR_FIELD, AGENT_BACKGROUND_PROCESSOR_INTEGRATION, AGENT_BACKGROUND_PROCESSOR_ROUTE, AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD, AGENT_CHAT_PROCESS_RUN_PATH, isDurableBackgroundFlagExplicitlyDisabled, } from "../agent/durable-background.js";
21
21
  import { INTEGRATION_RECOVERY_RUNTIME_MARKER, INTEGRATION_RETRY_SWEEP_PATH, INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT, isIntegrationDurableDispatchConfigured, } from "../integrations/integration-durable-dispatch-config.js";
22
+ import { RECURRING_JOBS_SWEEP_PATH, RECURRING_JOBS_SWEEP_TOKEN_SUBJECT, } from "../jobs/scheduler-dispatch.js";
22
23
  import { findWorkspaceRoot } from "../scripts/utils.js";
23
24
  import { DEFAULT_WORKSPACE_APP_AUDIENCE, normalizeWorkspaceAppAudience, normalizeWorkspaceAppPathList, workspaceAppAudienceFromPackageJson, workspaceAppRouteAccessFromPackageJson, } from "../shared/workspace-app-audience.js";
24
25
  import { DISPATCH_WORKSPACE_ROOT_REDIRECTS } from "../shared/workspace-app-id.js";
25
- import { assertEmittedBackgroundFunctionOnDisk } from "./build.js";
26
+ import { assertEmittedBackgroundFunctionOnDisk, isRecurringJobsDeployEnabled, } from "./build.js";
26
27
  import { cloneServerBundleForFunction } from "./function-bundle.js";
27
28
  import { collectImmutableAssetPaths, IMMUTABLE_ASSET_CACHE_HEADERS, } from "./immutable-assets.js";
28
29
  const NETLIFY_WORKSPACE_STATIC_DIR = "_workspace_static";
@@ -464,14 +465,93 @@ function copyNetlifyFunctionIntoWorkspace(workspaceRoot, app, workspaceApps, sta
464
465
  // Durable background agent runs. Additive ONLY: when explicitly opted out
465
466
  // this emits nothing and the single-function deploy is unchanged.
466
467
  const integrationDurableDispatch = app === "dispatch" && isIntegrationDurableDispatchConfigured();
468
+ const recurringJobs = isRecurringJobsDeployEnabled();
467
469
  if (isDurableBackgroundWorkspaceDeployEnabled() ||
468
- integrationDurableDispatch) {
470
+ integrationDurableDispatch ||
471
+ recurringJobs) {
469
472
  emitNetlifyBackgroundFunction(workspaceRoot, app, src, workspaceApps);
470
473
  }
474
+ if (recurringJobs) {
475
+ emitNetlifyRecurringJobsFunction(workspaceRoot, app);
476
+ }
471
477
  if (integrationDurableDispatch) {
472
478
  emitNetlifyIntegrationRecoveryFunction(workspaceRoot, app, src, workspaceApps);
473
479
  }
474
480
  }
481
+ /**
482
+ * Emit the per-app Netlify Scheduled Function that hands one recurring-job
483
+ * sweep to that app's durable background worker. The worker owns the actual
484
+ * scan so scheduled-function timeouts cannot strand the automation runner.
485
+ */
486
+ function emitNetlifyRecurringJobsFunction(workspaceRoot, app) {
487
+ const functionName = `${app}-agent-recurring-jobs`;
488
+ const dest = path.join(netlifyFunctionsDir(workspaceRoot), functionName);
489
+ const backgroundName = `${app}-agent-background`;
490
+ const backgroundPath = `/.netlify/functions/${backgroundName}`;
491
+ const sweepPath = `/${app}${RECURRING_JOBS_SWEEP_PATH}`;
492
+ fs.rmSync(dest, { recursive: true, force: true });
493
+ fs.mkdirSync(dest, { recursive: true });
494
+ const entry = `import { createHmac } from "node:crypto";
495
+
496
+ const BACKGROUND_PATH = ${JSON.stringify(backgroundPath)};
497
+ const SWEEP_PATH = ${JSON.stringify(sweepPath)};
498
+ const TOKEN_SUBJECT = ${JSON.stringify(RECURRING_JOBS_SWEEP_TOKEN_SUBJECT)};
499
+ const PROCESSOR_FIELD = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_FIELD)};
500
+ const PROCESSOR_ROUTE = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE)};
501
+ const PROCESSOR_ROUTE_FIELD = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD)};
502
+
503
+ function siteOrigin(request) {
504
+ const configured = process.env.URL || process.env.DEPLOY_URL;
505
+ if (configured) return configured;
506
+ return new URL(request.url).origin;
507
+ }
508
+
509
+ function token(secret) {
510
+ const timestamp = Date.now();
511
+ const signature = createHmac("sha256", secret)
512
+ .update(\`${RECURRING_JOBS_SWEEP_TOKEN_SUBJECT}:\${timestamp}\`)
513
+ .digest("hex");
514
+ return \`\${timestamp}.\${signature}\`;
515
+ }
516
+
517
+ export default async function handler(request) {
518
+ const secret = process.env.A2A_SECRET;
519
+ if (!secret) {
520
+ throw new Error("[recurring-jobs] A2A_SECRET is required for the scheduled sweep");
521
+ }
522
+ const url = new URL(BACKGROUND_PATH, siteOrigin(request));
523
+ const response = await fetch(url, {
524
+ method: "POST",
525
+ headers: {
526
+ Authorization: \`Bearer \${token(secret)}\`,
527
+ "Content-Type": "application/json",
528
+ "user-agent": "agent-native-recurring-jobs",
529
+ },
530
+ body: JSON.stringify({
531
+ [PROCESSOR_FIELD]: PROCESSOR_ROUTE,
532
+ [PROCESSOR_ROUTE_FIELD]: SWEEP_PATH,
533
+ }),
534
+ });
535
+ if (!response.ok) {
536
+ const body = await response.text();
537
+ throw new Error(
538
+ \`[recurring-jobs] Durable sweep handoff failed (\${response.status}): \${body.slice(0, 500)}\`,
539
+ );
540
+ }
541
+ console.log("[recurring-jobs] Durable sweep handed off", url.toString());
542
+ return new Response(null, { status: 204 });
543
+ }
544
+
545
+ export const config = {
546
+ name: ${JSON.stringify(`${app} agent-native recurring jobs`)},
547
+ generator: "agent-native workspace deploy",
548
+ schedule: "* * * * *",
549
+ nodeBundler: "none",
550
+ };
551
+ `;
552
+ fs.writeFileSync(path.join(dest, `${functionName}.mjs`), entry);
553
+ console.log(`[workspace-deploy] Emitted Netlify scheduled recurring-job function "${functionName}" for app "${app}".`);
554
+ }
475
555
  /**
476
556
  * Deploy-time gate for emitting the per-app `-background` Netlify function.
477
557
  *
@@ -534,6 +614,7 @@ function emitNetlifyBackgroundFunction(workspaceRoot, app, srcServerDir, workspa
534
614
  const processRunPath = `${basePath}${AGENT_CHAT_PROCESS_RUN_PATH}`;
535
615
  const a2aProcessTaskPath = `${basePath}/_agent-native/a2a/_process-task`;
536
616
  const integrationProcessTaskPath = `${basePath}/_agent-native/integrations/process-task`;
617
+ const recurringJobsSweepPath = `${basePath}${RECURRING_JOBS_SWEEP_PATH}`;
537
618
  const server = `// Mark this isolate as the durable background runtime BEFORE the handler bundle
538
619
  // is imported, so isInBackgroundFunctionRuntime() reliably returns true in this
539
620
  // function (the deployed Lambda name is not guaranteed to end in -background). A
@@ -546,6 +627,7 @@ const basePath = ${JSON.stringify(basePath)};
546
627
  const PROCESS_RUN_PATH = ${JSON.stringify(processRunPath)};
547
628
  const A2A_PROCESS_TASK_PATH = ${JSON.stringify(a2aProcessTaskPath)};
548
629
  const INTEGRATION_PROCESS_TASK_PATH = ${JSON.stringify(integrationProcessTaskPath)};
630
+ const RECURRING_JOBS_SWEEP_PATH = ${JSON.stringify(recurringJobsSweepPath)};
549
631
  const BACKGROUND_PROCESSOR_FIELD = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_FIELD)};
550
632
  const BACKGROUND_PROCESSOR_A2A = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_A2A)};
551
633
  const BACKGROUND_PROCESSOR_INTEGRATION = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_INTEGRATION)};
@@ -569,7 +651,8 @@ function processorPathFromBody(body) {
569
651
  if (
570
652
  parsed?.[BACKGROUND_PROCESSOR_FIELD] === BACKGROUND_PROCESSOR_ROUTE &&
571
653
  typeof route === "string" &&
572
- route.startsWith(basePath + "/api/_agent-native-background/") &&
654
+ (route === RECURRING_JOBS_SWEEP_PATH ||
655
+ route.startsWith(basePath + "/api/_agent-native-background/")) &&
573
656
  !route.includes("?") &&
574
657
  !route.includes("#")
575
658
  ) {
@@ -0,0 +1,5 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ templateId: string;
3
+ limit?: unknown;
4
+ }, import("../provider-metrics.js").ProviderMetricsResult<import("../provider-metrics.js").EmailActivityEntry[]>>;
5
+ export default _default;
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { getRequestOrgId } from "../../server/request-context.js";
4
+ import { authorizeTransactionalEmailRead } from "../authorize.js";
5
+ import { fetchEmailActivity } from "../provider-metrics.js";
6
+ export default defineAction({
7
+ description: "List recent provider activity for one registered transactional email in this app. Organization admin only.",
8
+ schema: z.object({
9
+ templateId: z.string().min(1),
10
+ limit: z.coerce.number().int().min(1).max(1000).default(50),
11
+ }),
12
+ http: { method: "GET" },
13
+ authorize: ({ templateId }) => authorizeTransactionalEmailRead([templateId]),
14
+ run: async ({ templateId, limit }) => fetchEmailActivity({
15
+ templateId,
16
+ limit,
17
+ orgId: getRequestOrgId(),
18
+ }),
19
+ });
@@ -0,0 +1,5 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ templateIds: string[];
3
+ windowDays?: unknown;
4
+ }, import("../provider-metrics.js").ProviderMetricsResult<import("../provider-metrics.js").EmailEngagement[]>>;
5
+ export default _default;
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { getRequestOrgId } from "../../server/request-context.js";
4
+ import { authorizeTransactionalEmailRead } from "../authorize.js";
5
+ import { fetchEmailEngagement } from "../provider-metrics.js";
6
+ export default defineAction({
7
+ description: "Read provider engagement metrics for registered transactional emails in this app. Organization admin only.",
8
+ schema: z.object({
9
+ templateIds: z.array(z.string().min(1)).max(100),
10
+ windowDays: z.coerce.number().int().min(1).max(365).default(30),
11
+ }),
12
+ http: { method: "POST" },
13
+ authorize: ({ templateIds }) => authorizeTransactionalEmailRead(templateIds),
14
+ run: async ({ templateIds, windowDays }) => fetchEmailEngagement(templateIds, windowDays, getRequestOrgId()),
15
+ });
@@ -0,0 +1,7 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ templateId?: string;
3
+ limit?: unknown;
4
+ }, {
5
+ entries: import("../log.js").EmailLogEntry[];
6
+ }>;
7
+ export default _default;
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { getAppSlug } from "../../server/app-name.js";
4
+ import { getRequestOrgId } from "../../server/request-context.js";
5
+ import { authorizeTransactionalEmailRead } from "../authorize.js";
6
+ import { listEmailLog } from "../log.js";
7
+ export default defineAction({
8
+ description: "List recent transactional email sends from this app, newest first, optionally filtered to one registered email id.",
9
+ schema: z.object({
10
+ templateId: z.string().optional(),
11
+ limit: z.coerce.number().int().min(1).max(500).default(100),
12
+ }),
13
+ http: { method: "GET" },
14
+ authorize: ({ templateId }) => authorizeTransactionalEmailRead(templateId ? [templateId] : []),
15
+ run: async ({ templateId, limit }) => ({
16
+ entries: await listEmailLog({
17
+ orgId: getRequestOrgId() ?? "",
18
+ app: getAppSlug() ?? "unknown",
19
+ templateId,
20
+ limit,
21
+ }),
22
+ }),
23
+ });
@@ -0,0 +1,22 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ windowDays?: unknown;
3
+ }, {
4
+ app: string;
5
+ windowDays: number;
6
+ statsAvailable: boolean;
7
+ statsError: string;
8
+ emails: {
9
+ id: string;
10
+ app: string;
11
+ name: string;
12
+ trigger: string;
13
+ recipient: string;
14
+ recipientLabel: string;
15
+ sender: string;
16
+ senderLabel: string;
17
+ sent: number;
18
+ failed: number;
19
+ lastSentAt: number;
20
+ }[];
21
+ }>;
22
+ export default _default;
@@ -0,0 +1,67 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { getAppSlug } from "../../server/app-name.js";
4
+ import { getRequestOrgId } from "../../server/request-context.js";
5
+ import { authorizeTransactionalEmailRead } from "../authorize.js";
6
+ import { getEmailSendStats } from "../log.js";
7
+ import { listTransactionalEmails } from "../registry.js";
8
+ import { registerCoreSystemEmails } from "../system-emails.js";
9
+ const DEFAULT_WINDOW_DAYS = 30;
10
+ export default defineAction({
11
+ description: "List the transactional emails this app can send, with the trigger, recipient and sender logic for each, plus local send counts and last-sent. Engagement metrics such as open rate are not included here — they live at the email provider.",
12
+ schema: z.object({
13
+ windowDays: z.coerce
14
+ .number()
15
+ .int()
16
+ .min(1)
17
+ .max(365)
18
+ .default(DEFAULT_WINDOW_DAYS)
19
+ .describe("How many days of send history to summarize."),
20
+ }),
21
+ http: { method: "GET" },
22
+ authorize: () => authorizeTransactionalEmailRead([]),
23
+ run: async ({ windowDays }) => {
24
+ registerCoreSystemEmails();
25
+ const since = Date.now() - windowDays * 24 * 60 * 60 * 1000;
26
+ const app = getAppSlug() ?? "unknown";
27
+ const orgId = getRequestOrgId();
28
+ const definitions = listTransactionalEmails();
29
+ // A failed stats read must not masquerade as "no email ever sent" — the
30
+ // catalog is still worth returning, but the caller has to be able to tell
31
+ // that the numbers are missing rather than zero.
32
+ let statsById = null;
33
+ let statsError = null;
34
+ try {
35
+ if (!orgId) {
36
+ throw new Error("Organization context is required for transactional email stats.");
37
+ }
38
+ const stats = await getEmailSendStats(since, app, orgId);
39
+ statsById = new Map(stats.map((row) => [row.templateId, row]));
40
+ }
41
+ catch (error) {
42
+ statsError = error instanceof Error ? error.message : String(error);
43
+ }
44
+ return {
45
+ app,
46
+ windowDays,
47
+ statsAvailable: statsError === null,
48
+ statsError,
49
+ emails: definitions.map((definition) => {
50
+ const stats = statsById?.get(definition.id);
51
+ return {
52
+ id: definition.id,
53
+ app: definition.app,
54
+ name: definition.name,
55
+ trigger: definition.trigger,
56
+ recipient: definition.recipient,
57
+ recipientLabel: definition.recipientLabel,
58
+ sender: definition.sender,
59
+ senderLabel: definition.senderLabel,
60
+ sent: stats?.sent ?? (statsById ? 0 : null),
61
+ failed: stats?.failed ?? (statsById ? 0 : null),
62
+ lastSentAt: stats?.lastSentAt ?? null,
63
+ };
64
+ }),
65
+ };
66
+ },
67
+ });
@@ -0,0 +1,9 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ id: string;
3
+ }, {
4
+ id: string;
5
+ subject: string;
6
+ html: string;
7
+ text: string;
8
+ }>;
9
+ export default _default;
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { authorizeTransactionalEmailRead } from "../authorize.js";
4
+ import { renderTransactionalEmailPreview } from "../registry.js";
5
+ import { registerCoreSystemEmails } from "../system-emails.js";
6
+ export default defineAction({
7
+ description: "Render one of this app's transactional emails with representative dummy data, returning the subject, HTML and plain-text bodies for preview.",
8
+ schema: z.object({
9
+ id: z
10
+ .string()
11
+ .describe("Registered email id, e.g. calendar.booking-confirmed."),
12
+ }),
13
+ http: { method: "GET" },
14
+ authorize: ({ id }) => authorizeTransactionalEmailRead([id]),
15
+ run: async ({ id }) => {
16
+ registerCoreSystemEmails();
17
+ const rendered = renderTransactionalEmailPreview(id);
18
+ return {
19
+ id,
20
+ subject: rendered.subject,
21
+ html: rendered.html,
22
+ text: rendered.text,
23
+ };
24
+ },
25
+ });
@@ -0,0 +1 @@
1
+ export declare function authorizeTransactionalEmailRead(templateIds?: string[]): Promise<boolean>;
@@ -0,0 +1,9 @@
1
+ import { currentRequestUserIsOrgAdmin } from "../server/org-admin.js";
2
+ import { getTransactionalEmail } from "./registry.js";
3
+ import { registerCoreSystemEmails } from "./system-emails.js";
4
+ export async function authorizeTransactionalEmailRead(templateIds = []) {
5
+ if (!(await currentRequestUserIsOrgAdmin()))
6
+ return false;
7
+ registerCoreSystemEmails();
8
+ return templateIds.every((id) => Boolean(getTransactionalEmail(id)));
9
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Durable send log for transactional emails.
3
+ *
4
+ * Written by `sendEmail` on every attempt, successful or not. Read by Dispatch
5
+ * to report per-email send counts and last-sent without depending on the
6
+ * provider's activity retention window.
7
+ */
8
+ export interface RecordEmailSendArgs {
9
+ orgId?: string | null;
10
+ templateId?: string;
11
+ app?: string;
12
+ recipient: string;
13
+ sender: string;
14
+ subject: string;
15
+ status: "sent" | "failed";
16
+ error?: string;
17
+ provider: string;
18
+ }
19
+ /**
20
+ * Append one send record.
21
+ *
22
+ * Callers treat logging as best-effort: a logging failure must not turn a
23
+ * delivered email into a thrown send. The failure is surfaced on the console
24
+ * rather than swallowed, so a persistently broken log is visible instead of
25
+ * quietly producing an empty activity view.
26
+ */
27
+ export declare function recordEmailSend(args: RecordEmailSendArgs): Promise<void>;
28
+ export interface EmailSendStats {
29
+ templateId: string;
30
+ sent: number;
31
+ failed: number;
32
+ lastSentAt: number | null;
33
+ }
34
+ /**
35
+ * Per-template send counts and last-sent, for sends at or after `since`.
36
+ * Templates with no rows are absent from the result — callers distinguish
37
+ * "never sent" from "sent zero times in window" by that absence.
38
+ */
39
+ export declare function getEmailSendStats(since: number, app: string, orgId: string): Promise<EmailSendStats[]>;
40
+ export interface EmailLogEntry {
41
+ id: string;
42
+ templateId: string | null;
43
+ app: string | null;
44
+ recipient: string;
45
+ sender: string;
46
+ subject: string;
47
+ status: string;
48
+ error: string | null;
49
+ provider: string;
50
+ createdAt: number;
51
+ }
52
+ /** Most recent sends for one app, newest first, optionally filtered to one template. */
53
+ export declare function listEmailLog(options: {
54
+ orgId: string;
55
+ app: string;
56
+ templateId?: string;
57
+ limit?: number;
58
+ }): Promise<EmailLogEntry[]>;
59
+ /** Provider category that is safe to query for one organization only. */
60
+ export declare function getScopedEmailProviderCategory(templateId: string, orgId: string): string;