@agent-native/core 0.119.5 → 0.119.6

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 (89) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/durable-background.ts +1 -0
  5. package/corpus/core/src/deploy/build.ts +144 -2
  6. package/corpus/core/src/deploy/workspace-deploy.ts +123 -1
  7. package/corpus/core/src/integrations/integration-durable-dispatch-config.ts +22 -0
  8. package/corpus/core/src/integrations/integration-durable-dispatch.ts +190 -0
  9. package/corpus/core/src/integrations/pending-tasks-retry-job.ts +175 -87
  10. package/corpus/core/src/integrations/pending-tasks-store.ts +114 -11
  11. package/corpus/core/src/integrations/plugin.ts +113 -60
  12. package/corpus/core/src/integrations/task-queue-stats.ts +68 -15
  13. package/corpus/core/src/integrations/webhook-handler.ts +21 -46
  14. package/corpus/core/src/server/auth.ts +6 -0
  15. package/corpus/core/src/templates/default/.agents/skills/integration-webhooks/SKILL.md +63 -22
  16. package/corpus/core/src/templates/headless/.agents/skills/integration-webhooks/SKILL.md +63 -22
  17. package/corpus/core/src/templates/workspace-core/.agents/skills/integration-webhooks/SKILL.md +63 -22
  18. package/corpus/templates/dispatch/changelog/2026-07-21-recover-stalled-slack-agent-work.md +6 -0
  19. package/dist/agent/durable-background.d.ts +1 -0
  20. package/dist/agent/durable-background.d.ts.map +1 -1
  21. package/dist/agent/durable-background.js +1 -0
  22. package/dist/agent/durable-background.js.map +1 -1
  23. package/dist/collab/routes.d.ts +1 -1
  24. package/dist/collab/struct-routes.d.ts +1 -1
  25. package/dist/deploy/build.d.ts +3 -0
  26. package/dist/deploy/build.d.ts.map +1 -1
  27. package/dist/deploy/build.js +106 -3
  28. package/dist/deploy/build.js.map +1 -1
  29. package/dist/deploy/workspace-deploy.d.ts.map +1 -1
  30. package/dist/deploy/workspace-deploy.js +103 -2
  31. package/dist/deploy/workspace-deploy.js.map +1 -1
  32. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  33. package/dist/integrations/integration-durable-dispatch-config.d.ts +7 -0
  34. package/dist/integrations/integration-durable-dispatch-config.d.ts.map +1 -0
  35. package/dist/integrations/integration-durable-dispatch-config.js +16 -0
  36. package/dist/integrations/integration-durable-dispatch-config.js.map +1 -0
  37. package/dist/integrations/integration-durable-dispatch.d.ts +23 -0
  38. package/dist/integrations/integration-durable-dispatch.d.ts.map +1 -0
  39. package/dist/integrations/integration-durable-dispatch.js +115 -0
  40. package/dist/integrations/integration-durable-dispatch.js.map +1 -0
  41. package/dist/integrations/pending-tasks-retry-job.d.ts +13 -1
  42. package/dist/integrations/pending-tasks-retry-job.d.ts.map +1 -1
  43. package/dist/integrations/pending-tasks-retry-job.js +133 -74
  44. package/dist/integrations/pending-tasks-retry-job.js.map +1 -1
  45. package/dist/integrations/pending-tasks-store.d.ts +14 -2
  46. package/dist/integrations/pending-tasks-store.d.ts.map +1 -1
  47. package/dist/integrations/pending-tasks-store.js +75 -11
  48. package/dist/integrations/pending-tasks-store.js.map +1 -1
  49. package/dist/integrations/plugin.d.ts.map +1 -1
  50. package/dist/integrations/plugin.js +88 -55
  51. package/dist/integrations/plugin.js.map +1 -1
  52. package/dist/integrations/task-queue-stats.d.ts +15 -3
  53. package/dist/integrations/task-queue-stats.d.ts.map +1 -1
  54. package/dist/integrations/task-queue-stats.js +45 -15
  55. package/dist/integrations/task-queue-stats.js.map +1 -1
  56. package/dist/integrations/webhook-handler.d.ts +1 -1
  57. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  58. package/dist/integrations/webhook-handler.js +18 -42
  59. package/dist/integrations/webhook-handler.js.map +1 -1
  60. package/dist/notifications/routes.d.ts +3 -3
  61. package/dist/observability/routes.d.ts +5 -5
  62. package/dist/progress/routes.d.ts +1 -1
  63. package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
  64. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  65. package/dist/resources/handlers.d.ts +1 -1
  66. package/dist/secrets/routes.d.ts +3 -3
  67. package/dist/server/auth.d.ts.map +1 -1
  68. package/dist/server/auth.js +5 -0
  69. package/dist/server/auth.js.map +1 -1
  70. package/dist/server/realtime-token.d.ts +1 -1
  71. package/dist/server/transcribe-voice.d.ts +1 -1
  72. package/dist/templates/default/.agents/skills/integration-webhooks/SKILL.md +63 -22
  73. package/dist/templates/headless/.agents/skills/integration-webhooks/SKILL.md +63 -22
  74. package/dist/templates/workspace-core/.agents/skills/integration-webhooks/SKILL.md +63 -22
  75. package/package.json +1 -1
  76. package/src/agent/durable-background.ts +1 -0
  77. package/src/deploy/build.ts +144 -2
  78. package/src/deploy/workspace-deploy.ts +123 -1
  79. package/src/integrations/integration-durable-dispatch-config.ts +22 -0
  80. package/src/integrations/integration-durable-dispatch.ts +190 -0
  81. package/src/integrations/pending-tasks-retry-job.ts +175 -87
  82. package/src/integrations/pending-tasks-store.ts +114 -11
  83. package/src/integrations/plugin.ts +113 -60
  84. package/src/integrations/task-queue-stats.ts +68 -15
  85. package/src/integrations/webhook-handler.ts +21 -46
  86. package/src/server/auth.ts +6 -0
  87. package/src/templates/default/.agents/skills/integration-webhooks/SKILL.md +63 -22
  88. package/src/templates/headless/.agents/skills/integration-webhooks/SKILL.md +63 -22
  89. package/src/templates/workspace-core/.agents/skills/integration-webhooks/SKILL.md +63 -22
package/corpus/README.md CHANGED
@@ -28,6 +28,6 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
 
29
29
  ## Generated Counts
30
30
 
31
- - core files: 1660
31
+ - core files: 1662
32
32
  - toolkit files: 159
33
- - template files: 6330
33
+ - template files: 6331
@@ -1,5 +1,11 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.119.6
4
+
5
+ ### Patch Changes
6
+
7
+ - f3c3523: Add an opt-in, scoped Netlify background handoff and scheduled recovery sweep for durable messaging-integration tasks.
8
+
3
9
  ## 0.119.5
4
10
 
5
11
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.119.5",
3
+ "version": "0.119.6",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -85,6 +85,7 @@ export const AGENT_BACKGROUND_FUNCTION_URL_PATH = `/.netlify/functions/${AGENT_B
85
85
  */
86
86
  export const AGENT_BACKGROUND_PROCESSOR_FIELD = "__agentNativeProcessor";
87
87
  export const AGENT_BACKGROUND_PROCESSOR_A2A = "a2a";
88
+ export const AGENT_BACKGROUND_PROCESSOR_INTEGRATION = "integration";
88
89
  export const AGENT_BACKGROUND_PROCESSOR_ROUTE = "route";
89
90
  export const AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD =
90
91
  "__agentNativeProcessorRoute";
@@ -24,10 +24,16 @@ import {
24
24
  AGENT_BACKGROUND_FUNCTION_NAME,
25
25
  AGENT_BACKGROUND_PROCESSOR_A2A,
26
26
  AGENT_BACKGROUND_PROCESSOR_FIELD,
27
+ AGENT_BACKGROUND_PROCESSOR_INTEGRATION,
27
28
  AGENT_BACKGROUND_PROCESSOR_ROUTE,
28
29
  AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD,
29
30
  AGENT_CHAT_PROCESS_RUN_PATH,
30
31
  } from "../agent/durable-background.js";
32
+ import {
33
+ INTEGRATION_RETRY_SWEEP_PATH,
34
+ INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT,
35
+ isIntegrationDurableDispatchConfigured,
36
+ } from "../integrations/integration-durable-dispatch-config.js";
31
37
  import { normalizeAppBasePath } from "../server/app-base-path.js";
32
38
  import {
33
39
  DEFAULT_SSR_CDN_CACHE_CONTROL,
@@ -2369,6 +2375,13 @@ export function isDurableBackgroundDeployEnabled(): boolean {
2369
2375
  return v === "1" || v === "true" || v === "yes" || v === "on";
2370
2376
  }
2371
2377
 
2378
+ export const NETLIFY_INTEGRATION_RECOVERY_FUNCTION_NAME =
2379
+ "server-integration-recovery";
2380
+
2381
+ export function isIntegrationDurableDispatchDeployEnabled(): boolean {
2382
+ return isIntegrationDurableDispatchConfigured();
2383
+ }
2384
+
2372
2385
  const NETLIFY_KEEP_WARM_FUNCTION_NAME = "agent-native-keep-warm";
2373
2386
 
2374
2387
  /**
@@ -2536,10 +2549,16 @@ export function emitSingleTemplateNetlifyBackgroundFunction(
2536
2549
 
2537
2550
  const processRunPath = JSON.stringify(AGENT_CHAT_PROCESS_RUN_PATH);
2538
2551
  const a2aProcessTaskPath = JSON.stringify("/_agent-native/a2a/_process-task");
2552
+ const integrationProcessTaskPath = JSON.stringify(
2553
+ "/_agent-native/integrations/process-task",
2554
+ );
2539
2555
  const backgroundProcessorField = JSON.stringify(
2540
2556
  AGENT_BACKGROUND_PROCESSOR_FIELD,
2541
2557
  );
2542
2558
  const backgroundProcessorA2A = JSON.stringify(AGENT_BACKGROUND_PROCESSOR_A2A);
2559
+ const backgroundProcessorIntegration = JSON.stringify(
2560
+ AGENT_BACKGROUND_PROCESSOR_INTEGRATION,
2561
+ );
2543
2562
  const backgroundProcessorRoute = JSON.stringify(
2544
2563
  AGENT_BACKGROUND_PROCESSOR_ROUTE,
2545
2564
  );
@@ -2558,8 +2577,10 @@ globalThis.__AGENT_NATIVE_BACKGROUND_RUNTIME__ = true;
2558
2577
  // The framework route the Nitro router dispatches to (the _process-run plugin).
2559
2578
  const PROCESS_RUN_PATH = ${processRunPath};
2560
2579
  const A2A_PROCESS_TASK_PATH = ${a2aProcessTaskPath};
2580
+ const INTEGRATION_PROCESS_TASK_PATH = ${integrationProcessTaskPath};
2561
2581
  const BACKGROUND_PROCESSOR_FIELD = ${backgroundProcessorField};
2562
2582
  const BACKGROUND_PROCESSOR_A2A = ${backgroundProcessorA2A};
2583
+ const BACKGROUND_PROCESSOR_INTEGRATION = ${backgroundProcessorIntegration};
2563
2584
  const BACKGROUND_PROCESSOR_ROUTE = ${backgroundProcessorRoute};
2564
2585
  const BACKGROUND_PROCESSOR_ROUTE_FIELD = ${backgroundProcessorRouteField};
2565
2586
 
@@ -2570,6 +2591,12 @@ function processorPathFromBody(body) {
2570
2591
  if (parsed?.[BACKGROUND_PROCESSOR_FIELD] === BACKGROUND_PROCESSOR_A2A) {
2571
2592
  return A2A_PROCESS_TASK_PATH;
2572
2593
  }
2594
+ if (
2595
+ parsed?.[BACKGROUND_PROCESSOR_FIELD] ===
2596
+ BACKGROUND_PROCESSOR_INTEGRATION
2597
+ ) {
2598
+ return INTEGRATION_PROCESS_TASK_PATH;
2599
+ }
2573
2600
  const route = parsed?.[BACKGROUND_PROCESSOR_ROUTE_FIELD];
2574
2601
  if (
2575
2602
  parsed?.[BACKGROUND_PROCESSOR_FIELD] === BACKGROUND_PROCESSOR_ROUTE &&
@@ -2658,6 +2685,78 @@ export const config = {
2658
2685
  );
2659
2686
  }
2660
2687
 
2688
+ export function emitSingleTemplateNetlifyIntegrationRecoveryFunction(
2689
+ projectCwd: string,
2690
+ ): void {
2691
+ const internalDir = path.join(projectCwd, ".netlify", "functions-internal");
2692
+ const serverDir = path.join(internalDir, "server");
2693
+ if (!fs.existsSync(path.join(serverDir, "main.mjs"))) {
2694
+ console.warn(
2695
+ "[build] Integration recovery emit skipped: expected Nitro Netlify function " +
2696
+ "at .netlify/functions-internal/server/main.mjs was not found.",
2697
+ );
2698
+ return;
2699
+ }
2700
+ const functionName = NETLIFY_INTEGRATION_RECOVERY_FUNCTION_NAME;
2701
+ const dest = path.join(internalDir, functionName);
2702
+ fs.rmSync(dest, { recursive: true, force: true });
2703
+ copyDir(serverDir, dest);
2704
+ fs.rmSync(path.join(dest, "server.mjs"), { force: true });
2705
+
2706
+ const entry = `import { createHmac } from "node:crypto";
2707
+
2708
+ const SWEEP_PATH = ${JSON.stringify(INTEGRATION_RETRY_SWEEP_PATH)};
2709
+ const SWEEP_SUBJECT = ${JSON.stringify(INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT)};
2710
+
2711
+ function enabled() {
2712
+ const raw = process.env.AGENT_INTEGRATION_DURABLE_DISPATCH;
2713
+ if (!raw) return false;
2714
+ return ["1", "true", "yes", "on"].includes(raw.trim().toLowerCase());
2715
+ }
2716
+
2717
+ function token(secret) {
2718
+ const timestamp = Date.now();
2719
+ const signature = createHmac("sha256", secret)
2720
+ .update(\`${INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT}:\${timestamp}\`)
2721
+ .digest("hex");
2722
+ return \`\${timestamp}.\${signature}\`;
2723
+ }
2724
+
2725
+ let cachedHandler;
2726
+
2727
+ export default async function handler(request, context) {
2728
+ if (!enabled()) return new Response(null, { status: 204 });
2729
+ const secret = process.env.A2A_SECRET;
2730
+ if (!secret) {
2731
+ console.error("[integration-recovery] A2A_SECRET is required; sweep skipped");
2732
+ return new Response(null, { status: 204 });
2733
+ }
2734
+ cachedHandler ??= (await import("./main.mjs")).default;
2735
+ const url = new URL(request.url);
2736
+ url.pathname = SWEEP_PATH;
2737
+ const rewritten = new Request(url.toString(), {
2738
+ method: "POST",
2739
+ headers: {
2740
+ Authorization: \`Bearer \${token(secret)}\`,
2741
+ "Content-Type": "application/json",
2742
+ },
2743
+ body: JSON.stringify({ taskId: SWEEP_SUBJECT }),
2744
+ });
2745
+ return cachedHandler(rewritten, context);
2746
+ }
2747
+
2748
+ export const config = {
2749
+ name: "integration pending-task recovery",
2750
+ generator: "agent-native build",
2751
+ schedule: "* * * * *",
2752
+ nodeBundler: "none",
2753
+ includedFiles: ["**"],
2754
+ preferStatic: false,
2755
+ };
2756
+ `;
2757
+ fs.writeFileSync(path.join(dest, `${functionName}.mjs`), entry);
2758
+ }
2759
+
2661
2760
  /**
2662
2761
  * Nitro's Netlify preset can emit a harmful fallback rewrite to
2663
2762
  * `/.netlify/functions/server`. With `config.path: "/*"`, that default URL is
@@ -2950,7 +3049,10 @@ export function assertSingleTemplateNetlifyBuildOutput(
2950
3049
  );
2951
3050
  }
2952
3051
 
2953
- if (isDurableBackgroundDeployEnabled()) {
3052
+ if (
3053
+ isDurableBackgroundDeployEnabled() ||
3054
+ isIntegrationDurableDispatchDeployEnabled()
3055
+ ) {
2954
3056
  const backgroundDir = path.join(
2955
3057
  internalDir,
2956
3058
  AGENT_BACKGROUND_FUNCTION_NAME,
@@ -2987,6 +3089,32 @@ export function assertSingleTemplateNetlifyBuildOutput(
2987
3089
  }
2988
3090
  }
2989
3091
 
3092
+ if (isIntegrationDurableDispatchDeployEnabled()) {
3093
+ const recoveryEntryPath = path.join(
3094
+ internalDir,
3095
+ NETLIFY_INTEGRATION_RECOVERY_FUNCTION_NAME,
3096
+ `${NETLIFY_INTEGRATION_RECOVERY_FUNCTION_NAME}.mjs`,
3097
+ );
3098
+ if (!fs.existsSync(recoveryEntryPath)) {
3099
+ failures.push(
3100
+ `integration durable dispatch is enabled but ${path.relative(
3101
+ projectCwd,
3102
+ recoveryEntryPath,
3103
+ )} was not emitted`,
3104
+ );
3105
+ } else {
3106
+ const recoveryEntry = fs.readFileSync(recoveryEntryPath, "utf-8");
3107
+ if (!/\bschedule\s*:\s*["']\* \* \* \* \*["']/.test(recoveryEntry)) {
3108
+ failures.push(
3109
+ `integration recovery entry ${path.relative(
3110
+ projectCwd,
3111
+ recoveryEntryPath,
3112
+ )} is missing the one-minute schedule`,
3113
+ );
3114
+ }
3115
+ }
3116
+ }
3117
+
2990
3118
  if (failures.length > 0) {
2991
3119
  throw new Error(
2992
3120
  "[deploy] Netlify deploy guard failed; refusing to publish an output " +
@@ -3638,7 +3766,10 @@ export default bundle;
3638
3766
  // bundle, so the chat `_process-run` POST lands on Netlify's async (15-min)
3639
3767
  // function. When not opted in this is a no-op and the single-function
3640
3768
  // deploy is byte-for-byte unchanged.
3641
- if (isDurableBackgroundDeployEnabled()) {
3769
+ if (
3770
+ isDurableBackgroundDeployEnabled() ||
3771
+ isIntegrationDurableDispatchDeployEnabled()
3772
+ ) {
3642
3773
  try {
3643
3774
  emitSingleTemplateNetlifyBackgroundFunction(cwd);
3644
3775
  } catch (err) {
@@ -3649,6 +3780,17 @@ export default bundle;
3649
3780
  }
3650
3781
  }
3651
3782
 
3783
+ if (isIntegrationDurableDispatchDeployEnabled()) {
3784
+ try {
3785
+ emitSingleTemplateNetlifyIntegrationRecoveryFunction(cwd);
3786
+ } catch (err) {
3787
+ console.warn(
3788
+ "[build] Failed to emit integration recovery Netlify function (non-fatal):",
3789
+ err instanceof Error ? err.message : err,
3790
+ );
3791
+ }
3792
+ }
3793
+
3652
3794
  writeSingleTemplateNetlifyRedirects(cwd);
3653
3795
  assertSingleTemplateNetlifyBuildOutput(cwd);
3654
3796
  }
@@ -21,10 +21,16 @@ import path from "path";
21
21
  import {
22
22
  AGENT_BACKGROUND_PROCESSOR_A2A,
23
23
  AGENT_BACKGROUND_PROCESSOR_FIELD,
24
+ AGENT_BACKGROUND_PROCESSOR_INTEGRATION,
24
25
  AGENT_BACKGROUND_PROCESSOR_ROUTE,
25
26
  AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD,
26
27
  AGENT_CHAT_PROCESS_RUN_PATH,
27
28
  } from "../agent/durable-background.js";
29
+ import {
30
+ INTEGRATION_RETRY_SWEEP_PATH,
31
+ INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT,
32
+ isIntegrationDurableDispatchConfigured,
33
+ } from "../integrations/integration-durable-dispatch-config.js";
28
34
  import { findWorkspaceRoot } from "../scripts/utils.js";
29
35
  import {
30
36
  DEFAULT_WORKSPACE_APP_AUDIENCE,
@@ -677,9 +683,19 @@ function copyNetlifyFunctionIntoWorkspace(
677
683
  // Durable background agent runs (default-ON; opt out with a falsy
678
684
  // AGENT_CHAT_DURABLE_BACKGROUND). Additive ONLY: when explicitly opted out
679
685
  // this emits nothing and the single-function deploy is unchanged.
680
- if (isDurableBackgroundDeployEnabled()) {
686
+ const integrationDurableDispatch =
687
+ app === "dispatch" && isIntegrationDurableDispatchConfigured();
688
+ if (isDurableBackgroundDeployEnabled() || integrationDurableDispatch) {
681
689
  emitNetlifyBackgroundFunction(workspaceRoot, app, src, workspaceApps);
682
690
  }
691
+ if (integrationDurableDispatch) {
692
+ emitNetlifyIntegrationRecoveryFunction(
693
+ workspaceRoot,
694
+ app,
695
+ src,
696
+ workspaceApps,
697
+ );
698
+ }
683
699
  }
684
700
 
685
701
  /**
@@ -753,6 +769,7 @@ function emitNetlifyBackgroundFunction(
753
769
  // incoming pathname to `/<app>/_agent-native/agent-chat/_process-run`.
754
770
  const processRunPath = `${basePath}${AGENT_CHAT_PROCESS_RUN_PATH}`;
755
771
  const a2aProcessTaskPath = `${basePath}/_agent-native/a2a/_process-task`;
772
+ const integrationProcessTaskPath = `${basePath}/_agent-native/integrations/process-task`;
756
773
  const server = `// Mark this isolate as the durable background runtime BEFORE the handler bundle
757
774
  // is imported, so isInBackgroundFunctionRuntime() reliably returns true in this
758
775
  // function (the deployed Lambda name is not guaranteed to end in -background). A
@@ -764,8 +781,10 @@ const basePath = ${JSON.stringify(basePath)};
764
781
  // The base-path-prefixed framework route the Nitro router dispatches to.
765
782
  const PROCESS_RUN_PATH = ${JSON.stringify(processRunPath)};
766
783
  const A2A_PROCESS_TASK_PATH = ${JSON.stringify(a2aProcessTaskPath)};
784
+ const INTEGRATION_PROCESS_TASK_PATH = ${JSON.stringify(integrationProcessTaskPath)};
767
785
  const BACKGROUND_PROCESSOR_FIELD = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_FIELD)};
768
786
  const BACKGROUND_PROCESSOR_A2A = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_A2A)};
787
+ const BACKGROUND_PROCESSOR_INTEGRATION = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_INTEGRATION)};
769
788
  const BACKGROUND_PROCESSOR_ROUTE = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE)};
770
789
  const BACKGROUND_PROCESSOR_ROUTE_FIELD = ${JSON.stringify(AGENT_BACKGROUND_PROCESSOR_ROUTE_FIELD)};
771
790
 
@@ -776,6 +795,12 @@ function processorPathFromBody(body) {
776
795
  if (parsed?.[BACKGROUND_PROCESSOR_FIELD] === BACKGROUND_PROCESSOR_A2A) {
777
796
  return A2A_PROCESS_TASK_PATH;
778
797
  }
798
+ if (
799
+ parsed?.[BACKGROUND_PROCESSOR_FIELD] ===
800
+ BACKGROUND_PROCESSOR_INTEGRATION
801
+ ) {
802
+ return INTEGRATION_PROCESS_TASK_PATH;
803
+ }
779
804
  const route = parsed?.[BACKGROUND_PROCESSOR_ROUTE_FIELD];
780
805
  if (
781
806
  parsed?.[BACKGROUND_PROCESSOR_FIELD] === BACKGROUND_PROCESSOR_ROUTE &&
@@ -862,6 +887,103 @@ export const config = {
862
887
  );
863
888
  }
864
889
 
890
+ function emitNetlifyIntegrationRecoveryFunction(
891
+ workspaceRoot: string,
892
+ app: string,
893
+ srcServerDir: string,
894
+ workspaceApps: WorkspaceAppManifestEntry[],
895
+ ): void {
896
+ const functionName = `${app}-integration-recovery`;
897
+ const dest = path.join(netlifyFunctionsDir(workspaceRoot), functionName);
898
+ fs.rmSync(dest, { recursive: true, force: true });
899
+ copyDir(srcServerDir, dest);
900
+ fs.rmSync(path.join(dest, "server.mjs"), { force: true });
901
+
902
+ const basePath = `/${app}`;
903
+ const workspaceAppAudience = workspaceAppAudienceForApp(workspaceApps, app);
904
+ const workspaceAppRouteAccess = workspaceAppRouteAccessForApp(
905
+ workspaceApps,
906
+ app,
907
+ );
908
+ const sweepPath = `${basePath}${INTEGRATION_RETRY_SWEEP_PATH}`;
909
+ const entry = `import { createHmac } from "node:crypto";
910
+
911
+ const basePath = ${JSON.stringify(basePath)};
912
+ const SWEEP_PATH = ${JSON.stringify(sweepPath)};
913
+ const SWEEP_SUBJECT = ${JSON.stringify(INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT)};
914
+
915
+ function setBasePathEnv() {
916
+ const processRef = globalThis.process ??= { env: {} };
917
+ processRef.env ??= {};
918
+ Object.assign(processRef.env, {
919
+ AGENT_NATIVE_WORKSPACE: "1",
920
+ AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
921
+ APP_BASE_PATH: basePath,
922
+ AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
923
+ AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
924
+ AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
925
+ VITE_AGENT_NATIVE_WORKSPACE: "1",
926
+ VITE_AGENT_NATIVE_WORKSPACE_APP_ID: ${JSON.stringify(app)},
927
+ VITE_APP_BASE_PATH: basePath,
928
+ VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: ${JSON.stringify(workspaceAppAudience)},
929
+ VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.publicPaths))},
930
+ VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: ${JSON.stringify(JSON.stringify(workspaceAppRouteAccess.protectedPaths))},
931
+ VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: ${JSON.stringify(JSON.stringify(workspaceApps))},
932
+ ${JSON.stringify(WORKSPACE_APPS_ENV_KEY)}: ${JSON.stringify(JSON.stringify(workspaceApps))},
933
+ });
934
+ }
935
+
936
+ function enabled() {
937
+ const raw = process.env.AGENT_INTEGRATION_DURABLE_DISPATCH;
938
+ if (!raw) return false;
939
+ return ["1", "true", "yes", "on"].includes(raw.trim().toLowerCase());
940
+ }
941
+
942
+ function token(secret) {
943
+ const timestamp = Date.now();
944
+ const signature = createHmac("sha256", secret)
945
+ .update(\`${INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT}:\${timestamp}\`)
946
+ .digest("hex");
947
+ return \`\${timestamp}.\${signature}\`;
948
+ }
949
+
950
+ setBasePathEnv();
951
+ let cachedHandler;
952
+
953
+ export default async function handler(request, context) {
954
+ setBasePathEnv();
955
+ if (!enabled()) return new Response(null, { status: 204 });
956
+ const secret = process.env.A2A_SECRET;
957
+ if (!secret) {
958
+ console.error("[integration-recovery] A2A_SECRET is required; sweep skipped");
959
+ return new Response(null, { status: 204 });
960
+ }
961
+ cachedHandler ??= (await import("./main.mjs")).default;
962
+ const url = new URL(request.url);
963
+ url.pathname = SWEEP_PATH;
964
+ const rewritten = new Request(url.toString(), {
965
+ method: "POST",
966
+ headers: {
967
+ Authorization: \`Bearer \${token(secret)}\`,
968
+ "Content-Type": "application/json",
969
+ },
970
+ body: JSON.stringify({ taskId: SWEEP_SUBJECT }),
971
+ });
972
+ return cachedHandler(rewritten, context);
973
+ }
974
+
975
+ export const config = {
976
+ name: ${JSON.stringify(`${app} integration pending-task recovery`)},
977
+ generator: "agent-native workspace deploy",
978
+ schedule: "* * * * *",
979
+ nodeBundler: "none",
980
+ includedFiles: ["**"],
981
+ preferStatic: false,
982
+ };
983
+ `;
984
+ fs.writeFileSync(path.join(dest, `${functionName}.mjs`), entry);
985
+ }
986
+
865
987
  function patchNetlifyFunctionEntry(
866
988
  functionDir: string,
867
989
  app: string,
@@ -0,0 +1,22 @@
1
+ export const INTEGRATION_DURABLE_DISPATCH_ENV =
2
+ "AGENT_INTEGRATION_DURABLE_DISPATCH";
3
+ export const INTEGRATION_DURABLE_DISPATCH_SCOPES_ENV =
4
+ "AGENT_INTEGRATION_DURABLE_DISPATCH_SCOPES";
5
+ export const INTEGRATION_PROCESS_TASK_PATH =
6
+ "/_agent-native/integrations/process-task";
7
+ export const INTEGRATION_RETRY_SWEEP_PATH =
8
+ "/_agent-native/integrations/retry-stuck-tasks";
9
+ export const INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT =
10
+ "integration-pending-tasks-sweep";
11
+
12
+ export function isIntegrationDurableDispatchConfigured(): boolean {
13
+ const value = process.env.AGENT_INTEGRATION_DURABLE_DISPATCH;
14
+ if (!value) return false;
15
+ const normalized = value.trim().toLowerCase();
16
+ return (
17
+ normalized === "1" ||
18
+ normalized === "true" ||
19
+ normalized === "yes" ||
20
+ normalized === "on"
21
+ );
22
+ }
@@ -0,0 +1,190 @@
1
+ import { hasConfiguredA2ASecret } from "../a2a/auth-policy.js";
2
+ import {
3
+ AGENT_BACKGROUND_PROCESSOR_FIELD,
4
+ AGENT_BACKGROUND_PROCESSOR_INTEGRATION,
5
+ dispatchPathTargetsNetlifyBackgroundFunction,
6
+ resolveDurableBackgroundDispatchPath,
7
+ } from "../agent/durable-background.js";
8
+ import { fireInternalDispatch } from "../server/self-dispatch.js";
9
+ import {
10
+ INTEGRATION_DURABLE_DISPATCH_ENV,
11
+ INTEGRATION_DURABLE_DISPATCH_SCOPES_ENV,
12
+ INTEGRATION_PROCESS_TASK_PATH,
13
+ INTEGRATION_RETRY_SWEEP_PATH,
14
+ INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT,
15
+ isIntegrationDurableDispatchConfigured,
16
+ } from "./integration-durable-dispatch-config.js";
17
+ import { recordPendingTaskDispatchAttempt } from "./pending-tasks-store.js";
18
+
19
+ export {
20
+ INTEGRATION_DURABLE_DISPATCH_ENV,
21
+ INTEGRATION_DURABLE_DISPATCH_SCOPES_ENV,
22
+ INTEGRATION_PROCESS_TASK_PATH,
23
+ INTEGRATION_RETRY_SWEEP_PATH,
24
+ INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT,
25
+ isIntegrationDurableDispatchConfigured,
26
+ };
27
+
28
+ export type IntegrationDispatchOutcome =
29
+ | "background-acknowledged"
30
+ | "portable-unconfirmed"
31
+ | "failed";
32
+
33
+ export interface IntegrationDispatchTaskScope {
34
+ platform: string;
35
+ externalThreadId: string;
36
+ platformContext?: Record<string, unknown>;
37
+ }
38
+
39
+ export interface IntegrationDurableDispatchScope {
40
+ platform: string;
41
+ value: string;
42
+ }
43
+
44
+ export function integrationDispatchScopeValue(
45
+ task: IntegrationDispatchTaskScope,
46
+ ): string | null {
47
+ const value = task.platformContext?.channelId;
48
+ return typeof value === "string" && value.trim() ? value.trim() : null;
49
+ }
50
+
51
+ export function configuredIntegrationDurableDispatchScopes():
52
+ | IntegrationDurableDispatchScope[]
53
+ | null {
54
+ const raw = process.env.AGENT_INTEGRATION_DURABLE_DISPATCH_SCOPES;
55
+ if (!raw?.trim()) return null;
56
+ return raw
57
+ .split(",")
58
+ .map((value) => value.trim())
59
+ .filter(Boolean)
60
+ .map((scope) => {
61
+ const separator = scope.indexOf(":");
62
+ return {
63
+ platform: separator > 0 ? scope.slice(0, separator) : "",
64
+ value: separator > 0 ? scope.slice(separator + 1) : "",
65
+ };
66
+ })
67
+ .filter((scope) => scope.platform && scope.value);
68
+ }
69
+
70
+ function taskScopeCandidates(task: IntegrationDispatchTaskScope): string[] {
71
+ const candidates = [
72
+ `${task.platform}:*`,
73
+ `${task.platform}:${task.externalThreadId}`,
74
+ ];
75
+ const explicitScope = integrationDispatchScopeValue(task);
76
+ if (explicitScope) {
77
+ candidates.push(`${task.platform}:${explicitScope}`);
78
+ } else if (task.platform === "slack") {
79
+ const channelId = task.externalThreadId.split(":")[2];
80
+ if (channelId) candidates.push(`slack:${channelId}`);
81
+ }
82
+ return candidates;
83
+ }
84
+
85
+ export function isIntegrationDurableDispatchEnabledForTask(
86
+ task: IntegrationDispatchTaskScope,
87
+ ): boolean {
88
+ if (!isIntegrationDurableDispatchConfigured()) return false;
89
+ const scopes = configuredIntegrationDurableDispatchScopes();
90
+ if (
91
+ scopes &&
92
+ !taskScopeCandidates(task).some((candidate) =>
93
+ scopes.some((scope) => `${scope.platform}:${scope.value}` === candidate),
94
+ )
95
+ ) {
96
+ return false;
97
+ }
98
+ const path = resolveDurableBackgroundDispatchPath(
99
+ INTEGRATION_PROCESS_TASK_PATH,
100
+ );
101
+ return (
102
+ dispatchPathTargetsNetlifyBackgroundFunction(path) &&
103
+ hasConfiguredA2ASecret()
104
+ );
105
+ }
106
+
107
+ async function recordDispatch(
108
+ taskId: string,
109
+ outcome: IntegrationDispatchOutcome,
110
+ ): Promise<void> {
111
+ try {
112
+ await recordPendingTaskDispatchAttempt(taskId, outcome);
113
+ } catch (error) {
114
+ console.error(
115
+ `[integrations] Failed to record dispatch outcome for ${taskId}:`,
116
+ error,
117
+ );
118
+ }
119
+ }
120
+
121
+ function logDispatch(
122
+ taskId: string,
123
+ outcome: IntegrationDispatchOutcome,
124
+ startedAt: number,
125
+ ): void {
126
+ console.info("[integrations] pending task dispatch", {
127
+ taskId,
128
+ outcome,
129
+ durationMs: Date.now() - startedAt,
130
+ });
131
+ }
132
+
133
+ export async function dispatchPendingIntegrationTask(input: {
134
+ taskId: string;
135
+ task: IntegrationDispatchTaskScope;
136
+ event?: unknown;
137
+ baseUrl?: string;
138
+ portableSettleMs?: number;
139
+ }): Promise<IntegrationDispatchOutcome> {
140
+ const startedAt = Date.now();
141
+ const durable = isIntegrationDurableDispatchEnabledForTask(input.task);
142
+ if (durable) {
143
+ const backgroundPath = resolveDurableBackgroundDispatchPath(
144
+ INTEGRATION_PROCESS_TASK_PATH,
145
+ );
146
+ try {
147
+ await fireInternalDispatch({
148
+ event: input.event,
149
+ baseUrl: input.baseUrl,
150
+ path: backgroundPath,
151
+ taskId: input.taskId,
152
+ body: {
153
+ [AGENT_BACKGROUND_PROCESSOR_FIELD]:
154
+ AGENT_BACKGROUND_PROCESSOR_INTEGRATION,
155
+ },
156
+ awaitResponse: true,
157
+ responseTimeoutMs: 2_000,
158
+ });
159
+ await recordDispatch(input.taskId, "background-acknowledged");
160
+ logDispatch(input.taskId, "background-acknowledged", startedAt);
161
+ return "background-acknowledged";
162
+ } catch (error) {
163
+ console.error(
164
+ `[integrations] Background dispatch failed for ${input.taskId}; trying the portable processor:`,
165
+ error,
166
+ );
167
+ }
168
+ }
169
+
170
+ try {
171
+ await fireInternalDispatch({
172
+ event: input.event,
173
+ baseUrl: input.baseUrl,
174
+ path: INTEGRATION_PROCESS_TASK_PATH,
175
+ taskId: input.taskId,
176
+ settleMs: input.portableSettleMs,
177
+ });
178
+ await recordDispatch(input.taskId, "portable-unconfirmed");
179
+ logDispatch(input.taskId, "portable-unconfirmed", startedAt);
180
+ return "portable-unconfirmed";
181
+ } catch (error) {
182
+ console.error(
183
+ `[integrations] Portable dispatch failed for ${input.taskId}:`,
184
+ error,
185
+ );
186
+ await recordDispatch(input.taskId, "failed");
187
+ logDispatch(input.taskId, "failed", startedAt);
188
+ return "failed";
189
+ }
190
+ }