@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
@@ -17,12 +17,12 @@ declare const _default: import("../../action.js").ActionDefinition<{
17
17
  id?: undefined;
18
18
  provider?: undefined;
19
19
  } | {
20
- error?: undefined;
21
20
  configured?: undefined;
22
21
  connectPath?: undefined;
23
22
  url: string;
24
23
  id: string;
25
24
  provider: string;
25
+ error?: undefined;
26
26
  }>;
27
27
  export default _default;
28
28
  //# sourceMappingURL=upload-image.d.ts.map
@@ -0,0 +1,7 @@
1
+ export declare const INTEGRATION_DURABLE_DISPATCH_ENV = "AGENT_INTEGRATION_DURABLE_DISPATCH";
2
+ export declare const INTEGRATION_DURABLE_DISPATCH_SCOPES_ENV = "AGENT_INTEGRATION_DURABLE_DISPATCH_SCOPES";
3
+ export declare const INTEGRATION_PROCESS_TASK_PATH = "/_agent-native/integrations/process-task";
4
+ export declare const INTEGRATION_RETRY_SWEEP_PATH = "/_agent-native/integrations/retry-stuck-tasks";
5
+ export declare const INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT = "integration-pending-tasks-sweep";
6
+ export declare function isIntegrationDurableDispatchConfigured(): boolean;
7
+ //# sourceMappingURL=integration-durable-dispatch-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration-durable-dispatch-config.d.ts","sourceRoot":"","sources":["../../src/integrations/integration-durable-dispatch-config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gCAAgC,uCACP,CAAC;AACvC,eAAO,MAAM,uCAAuC,8CACP,CAAC;AAC9C,eAAO,MAAM,6BAA6B,6CACE,CAAC;AAC7C,eAAO,MAAM,4BAA4B,kDACQ,CAAC;AAClD,eAAO,MAAM,qCAAqC,oCACf,CAAC;AAEpC,wBAAgB,sCAAsC,IAAI,OAAO,CAUhE"}
@@ -0,0 +1,16 @@
1
+ export const INTEGRATION_DURABLE_DISPATCH_ENV = "AGENT_INTEGRATION_DURABLE_DISPATCH";
2
+ export const INTEGRATION_DURABLE_DISPATCH_SCOPES_ENV = "AGENT_INTEGRATION_DURABLE_DISPATCH_SCOPES";
3
+ export const INTEGRATION_PROCESS_TASK_PATH = "/_agent-native/integrations/process-task";
4
+ export const INTEGRATION_RETRY_SWEEP_PATH = "/_agent-native/integrations/retry-stuck-tasks";
5
+ export const INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT = "integration-pending-tasks-sweep";
6
+ export function isIntegrationDurableDispatchConfigured() {
7
+ const value = process.env.AGENT_INTEGRATION_DURABLE_DISPATCH;
8
+ if (!value)
9
+ return false;
10
+ const normalized = value.trim().toLowerCase();
11
+ return (normalized === "1" ||
12
+ normalized === "true" ||
13
+ normalized === "yes" ||
14
+ normalized === "on");
15
+ }
16
+ //# sourceMappingURL=integration-durable-dispatch-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration-durable-dispatch-config.js","sourceRoot":"","sources":["../../src/integrations/integration-durable-dispatch-config.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gCAAgC,GAC3C,oCAAoC,CAAC;AACvC,MAAM,CAAC,MAAM,uCAAuC,GAClD,2CAA2C,CAAC;AAC9C,MAAM,CAAC,MAAM,6BAA6B,GACxC,0CAA0C,CAAC;AAC7C,MAAM,CAAC,MAAM,4BAA4B,GACvC,+CAA+C,CAAC;AAClD,MAAM,CAAC,MAAM,qCAAqC,GAChD,iCAAiC,CAAC;AAEpC,MAAM,UAAU,sCAAsC;IACpD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC;IAC7D,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC9C,OAAO,CACL,UAAU,KAAK,GAAG;QAClB,UAAU,KAAK,MAAM;QACrB,UAAU,KAAK,KAAK;QACpB,UAAU,KAAK,IAAI,CACpB,CAAC;AACJ,CAAC","sourcesContent":["export const INTEGRATION_DURABLE_DISPATCH_ENV =\n \"AGENT_INTEGRATION_DURABLE_DISPATCH\";\nexport const INTEGRATION_DURABLE_DISPATCH_SCOPES_ENV =\n \"AGENT_INTEGRATION_DURABLE_DISPATCH_SCOPES\";\nexport const INTEGRATION_PROCESS_TASK_PATH =\n \"/_agent-native/integrations/process-task\";\nexport const INTEGRATION_RETRY_SWEEP_PATH =\n \"/_agent-native/integrations/retry-stuck-tasks\";\nexport const INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT =\n \"integration-pending-tasks-sweep\";\n\nexport function isIntegrationDurableDispatchConfigured(): boolean {\n const value = process.env.AGENT_INTEGRATION_DURABLE_DISPATCH;\n if (!value) return false;\n const normalized = value.trim().toLowerCase();\n return (\n normalized === \"1\" ||\n normalized === \"true\" ||\n normalized === \"yes\" ||\n normalized === \"on\"\n );\n}\n"]}
@@ -0,0 +1,23 @@
1
+ import { INTEGRATION_DURABLE_DISPATCH_ENV, INTEGRATION_DURABLE_DISPATCH_SCOPES_ENV, INTEGRATION_PROCESS_TASK_PATH, INTEGRATION_RETRY_SWEEP_PATH, INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT, isIntegrationDurableDispatchConfigured } from "./integration-durable-dispatch-config.js";
2
+ export { INTEGRATION_DURABLE_DISPATCH_ENV, INTEGRATION_DURABLE_DISPATCH_SCOPES_ENV, INTEGRATION_PROCESS_TASK_PATH, INTEGRATION_RETRY_SWEEP_PATH, INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT, isIntegrationDurableDispatchConfigured, };
3
+ export type IntegrationDispatchOutcome = "background-acknowledged" | "portable-unconfirmed" | "failed";
4
+ export interface IntegrationDispatchTaskScope {
5
+ platform: string;
6
+ externalThreadId: string;
7
+ platformContext?: Record<string, unknown>;
8
+ }
9
+ export interface IntegrationDurableDispatchScope {
10
+ platform: string;
11
+ value: string;
12
+ }
13
+ export declare function integrationDispatchScopeValue(task: IntegrationDispatchTaskScope): string | null;
14
+ export declare function configuredIntegrationDurableDispatchScopes(): IntegrationDurableDispatchScope[] | null;
15
+ export declare function isIntegrationDurableDispatchEnabledForTask(task: IntegrationDispatchTaskScope): boolean;
16
+ export declare function dispatchPendingIntegrationTask(input: {
17
+ taskId: string;
18
+ task: IntegrationDispatchTaskScope;
19
+ event?: unknown;
20
+ baseUrl?: string;
21
+ portableSettleMs?: number;
22
+ }): Promise<IntegrationDispatchOutcome>;
23
+ //# sourceMappingURL=integration-durable-dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration-durable-dispatch.d.ts","sourceRoot":"","sources":["../../src/integrations/integration-durable-dispatch.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,gCAAgC,EAChC,uCAAuC,EACvC,6BAA6B,EAC7B,4BAA4B,EAC5B,qCAAqC,EACrC,sCAAsC,EACvC,MAAM,0CAA0C,CAAC;AAGlD,OAAO,EACL,gCAAgC,EAChC,uCAAuC,EACvC,6BAA6B,EAC7B,4BAA4B,EAC5B,qCAAqC,EACrC,sCAAsC,GACvC,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAClC,yBAAyB,GACzB,sBAAsB,GACtB,QAAQ,CAAC;AAEb,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,4BAA4B,GACjC,MAAM,GAAG,IAAI,CAGf;AAED,wBAAgB,0CAA0C,IACtD,+BAA+B,EAAE,GACjC,IAAI,CAeP;AAiBD,wBAAgB,0CAA0C,CACxD,IAAI,EAAE,4BAA4B,GACjC,OAAO,CAkBT;AA4BD,wBAAsB,8BAA8B,CAAC,KAAK,EAAE;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,4BAA4B,CAAC;IACnC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAmDtC"}
@@ -0,0 +1,115 @@
1
+ import { hasConfiguredA2ASecret } from "../a2a/auth-policy.js";
2
+ import { AGENT_BACKGROUND_PROCESSOR_FIELD, AGENT_BACKGROUND_PROCESSOR_INTEGRATION, dispatchPathTargetsNetlifyBackgroundFunction, resolveDurableBackgroundDispatchPath, } from "../agent/durable-background.js";
3
+ import { fireInternalDispatch } from "../server/self-dispatch.js";
4
+ import { INTEGRATION_DURABLE_DISPATCH_ENV, INTEGRATION_DURABLE_DISPATCH_SCOPES_ENV, INTEGRATION_PROCESS_TASK_PATH, INTEGRATION_RETRY_SWEEP_PATH, INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT, isIntegrationDurableDispatchConfigured, } from "./integration-durable-dispatch-config.js";
5
+ import { recordPendingTaskDispatchAttempt } from "./pending-tasks-store.js";
6
+ export { INTEGRATION_DURABLE_DISPATCH_ENV, INTEGRATION_DURABLE_DISPATCH_SCOPES_ENV, INTEGRATION_PROCESS_TASK_PATH, INTEGRATION_RETRY_SWEEP_PATH, INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT, isIntegrationDurableDispatchConfigured, };
7
+ export function integrationDispatchScopeValue(task) {
8
+ const value = task.platformContext?.channelId;
9
+ return typeof value === "string" && value.trim() ? value.trim() : null;
10
+ }
11
+ export function configuredIntegrationDurableDispatchScopes() {
12
+ const raw = process.env.AGENT_INTEGRATION_DURABLE_DISPATCH_SCOPES;
13
+ if (!raw?.trim())
14
+ return null;
15
+ return raw
16
+ .split(",")
17
+ .map((value) => value.trim())
18
+ .filter(Boolean)
19
+ .map((scope) => {
20
+ const separator = scope.indexOf(":");
21
+ return {
22
+ platform: separator > 0 ? scope.slice(0, separator) : "",
23
+ value: separator > 0 ? scope.slice(separator + 1) : "",
24
+ };
25
+ })
26
+ .filter((scope) => scope.platform && scope.value);
27
+ }
28
+ function taskScopeCandidates(task) {
29
+ const candidates = [
30
+ `${task.platform}:*`,
31
+ `${task.platform}:${task.externalThreadId}`,
32
+ ];
33
+ const explicitScope = integrationDispatchScopeValue(task);
34
+ if (explicitScope) {
35
+ candidates.push(`${task.platform}:${explicitScope}`);
36
+ }
37
+ else if (task.platform === "slack") {
38
+ const channelId = task.externalThreadId.split(":")[2];
39
+ if (channelId)
40
+ candidates.push(`slack:${channelId}`);
41
+ }
42
+ return candidates;
43
+ }
44
+ export function isIntegrationDurableDispatchEnabledForTask(task) {
45
+ if (!isIntegrationDurableDispatchConfigured())
46
+ return false;
47
+ const scopes = configuredIntegrationDurableDispatchScopes();
48
+ if (scopes &&
49
+ !taskScopeCandidates(task).some((candidate) => scopes.some((scope) => `${scope.platform}:${scope.value}` === candidate))) {
50
+ return false;
51
+ }
52
+ const path = resolveDurableBackgroundDispatchPath(INTEGRATION_PROCESS_TASK_PATH);
53
+ return (dispatchPathTargetsNetlifyBackgroundFunction(path) &&
54
+ hasConfiguredA2ASecret());
55
+ }
56
+ async function recordDispatch(taskId, outcome) {
57
+ try {
58
+ await recordPendingTaskDispatchAttempt(taskId, outcome);
59
+ }
60
+ catch (error) {
61
+ console.error(`[integrations] Failed to record dispatch outcome for ${taskId}:`, error);
62
+ }
63
+ }
64
+ function logDispatch(taskId, outcome, startedAt) {
65
+ console.info("[integrations] pending task dispatch", {
66
+ taskId,
67
+ outcome,
68
+ durationMs: Date.now() - startedAt,
69
+ });
70
+ }
71
+ export async function dispatchPendingIntegrationTask(input) {
72
+ const startedAt = Date.now();
73
+ const durable = isIntegrationDurableDispatchEnabledForTask(input.task);
74
+ if (durable) {
75
+ const backgroundPath = resolveDurableBackgroundDispatchPath(INTEGRATION_PROCESS_TASK_PATH);
76
+ try {
77
+ await fireInternalDispatch({
78
+ event: input.event,
79
+ baseUrl: input.baseUrl,
80
+ path: backgroundPath,
81
+ taskId: input.taskId,
82
+ body: {
83
+ [AGENT_BACKGROUND_PROCESSOR_FIELD]: AGENT_BACKGROUND_PROCESSOR_INTEGRATION,
84
+ },
85
+ awaitResponse: true,
86
+ responseTimeoutMs: 2_000,
87
+ });
88
+ await recordDispatch(input.taskId, "background-acknowledged");
89
+ logDispatch(input.taskId, "background-acknowledged", startedAt);
90
+ return "background-acknowledged";
91
+ }
92
+ catch (error) {
93
+ console.error(`[integrations] Background dispatch failed for ${input.taskId}; trying the portable processor:`, error);
94
+ }
95
+ }
96
+ try {
97
+ await fireInternalDispatch({
98
+ event: input.event,
99
+ baseUrl: input.baseUrl,
100
+ path: INTEGRATION_PROCESS_TASK_PATH,
101
+ taskId: input.taskId,
102
+ settleMs: input.portableSettleMs,
103
+ });
104
+ await recordDispatch(input.taskId, "portable-unconfirmed");
105
+ logDispatch(input.taskId, "portable-unconfirmed", startedAt);
106
+ return "portable-unconfirmed";
107
+ }
108
+ catch (error) {
109
+ console.error(`[integrations] Portable dispatch failed for ${input.taskId}:`, error);
110
+ await recordDispatch(input.taskId, "failed");
111
+ logDispatch(input.taskId, "failed", startedAt);
112
+ return "failed";
113
+ }
114
+ }
115
+ //# sourceMappingURL=integration-durable-dispatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration-durable-dispatch.js","sourceRoot":"","sources":["../../src/integrations/integration-durable-dispatch.ts"],"names":[],"mappings":"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","sourcesContent":["import { hasConfiguredA2ASecret } from \"../a2a/auth-policy.js\";\nimport {\n AGENT_BACKGROUND_PROCESSOR_FIELD,\n AGENT_BACKGROUND_PROCESSOR_INTEGRATION,\n dispatchPathTargetsNetlifyBackgroundFunction,\n resolveDurableBackgroundDispatchPath,\n} from \"../agent/durable-background.js\";\nimport { fireInternalDispatch } from \"../server/self-dispatch.js\";\nimport {\n INTEGRATION_DURABLE_DISPATCH_ENV,\n INTEGRATION_DURABLE_DISPATCH_SCOPES_ENV,\n INTEGRATION_PROCESS_TASK_PATH,\n INTEGRATION_RETRY_SWEEP_PATH,\n INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT,\n isIntegrationDurableDispatchConfigured,\n} from \"./integration-durable-dispatch-config.js\";\nimport { recordPendingTaskDispatchAttempt } from \"./pending-tasks-store.js\";\n\nexport {\n INTEGRATION_DURABLE_DISPATCH_ENV,\n INTEGRATION_DURABLE_DISPATCH_SCOPES_ENV,\n INTEGRATION_PROCESS_TASK_PATH,\n INTEGRATION_RETRY_SWEEP_PATH,\n INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT,\n isIntegrationDurableDispatchConfigured,\n};\n\nexport type IntegrationDispatchOutcome =\n | \"background-acknowledged\"\n | \"portable-unconfirmed\"\n | \"failed\";\n\nexport interface IntegrationDispatchTaskScope {\n platform: string;\n externalThreadId: string;\n platformContext?: Record<string, unknown>;\n}\n\nexport interface IntegrationDurableDispatchScope {\n platform: string;\n value: string;\n}\n\nexport function integrationDispatchScopeValue(\n task: IntegrationDispatchTaskScope,\n): string | null {\n const value = task.platformContext?.channelId;\n return typeof value === \"string\" && value.trim() ? value.trim() : null;\n}\n\nexport function configuredIntegrationDurableDispatchScopes():\n | IntegrationDurableDispatchScope[]\n | null {\n const raw = process.env.AGENT_INTEGRATION_DURABLE_DISPATCH_SCOPES;\n if (!raw?.trim()) return null;\n return raw\n .split(\",\")\n .map((value) => value.trim())\n .filter(Boolean)\n .map((scope) => {\n const separator = scope.indexOf(\":\");\n return {\n platform: separator > 0 ? scope.slice(0, separator) : \"\",\n value: separator > 0 ? scope.slice(separator + 1) : \"\",\n };\n })\n .filter((scope) => scope.platform && scope.value);\n}\n\nfunction taskScopeCandidates(task: IntegrationDispatchTaskScope): string[] {\n const candidates = [\n `${task.platform}:*`,\n `${task.platform}:${task.externalThreadId}`,\n ];\n const explicitScope = integrationDispatchScopeValue(task);\n if (explicitScope) {\n candidates.push(`${task.platform}:${explicitScope}`);\n } else if (task.platform === \"slack\") {\n const channelId = task.externalThreadId.split(\":\")[2];\n if (channelId) candidates.push(`slack:${channelId}`);\n }\n return candidates;\n}\n\nexport function isIntegrationDurableDispatchEnabledForTask(\n task: IntegrationDispatchTaskScope,\n): boolean {\n if (!isIntegrationDurableDispatchConfigured()) return false;\n const scopes = configuredIntegrationDurableDispatchScopes();\n if (\n scopes &&\n !taskScopeCandidates(task).some((candidate) =>\n scopes.some((scope) => `${scope.platform}:${scope.value}` === candidate),\n )\n ) {\n return false;\n }\n const path = resolveDurableBackgroundDispatchPath(\n INTEGRATION_PROCESS_TASK_PATH,\n );\n return (\n dispatchPathTargetsNetlifyBackgroundFunction(path) &&\n hasConfiguredA2ASecret()\n );\n}\n\nasync function recordDispatch(\n taskId: string,\n outcome: IntegrationDispatchOutcome,\n): Promise<void> {\n try {\n await recordPendingTaskDispatchAttempt(taskId, outcome);\n } catch (error) {\n console.error(\n `[integrations] Failed to record dispatch outcome for ${taskId}:`,\n error,\n );\n }\n}\n\nfunction logDispatch(\n taskId: string,\n outcome: IntegrationDispatchOutcome,\n startedAt: number,\n): void {\n console.info(\"[integrations] pending task dispatch\", {\n taskId,\n outcome,\n durationMs: Date.now() - startedAt,\n });\n}\n\nexport async function dispatchPendingIntegrationTask(input: {\n taskId: string;\n task: IntegrationDispatchTaskScope;\n event?: unknown;\n baseUrl?: string;\n portableSettleMs?: number;\n}): Promise<IntegrationDispatchOutcome> {\n const startedAt = Date.now();\n const durable = isIntegrationDurableDispatchEnabledForTask(input.task);\n if (durable) {\n const backgroundPath = resolveDurableBackgroundDispatchPath(\n INTEGRATION_PROCESS_TASK_PATH,\n );\n try {\n await fireInternalDispatch({\n event: input.event,\n baseUrl: input.baseUrl,\n path: backgroundPath,\n taskId: input.taskId,\n body: {\n [AGENT_BACKGROUND_PROCESSOR_FIELD]:\n AGENT_BACKGROUND_PROCESSOR_INTEGRATION,\n },\n awaitResponse: true,\n responseTimeoutMs: 2_000,\n });\n await recordDispatch(input.taskId, \"background-acknowledged\");\n logDispatch(input.taskId, \"background-acknowledged\", startedAt);\n return \"background-acknowledged\";\n } catch (error) {\n console.error(\n `[integrations] Background dispatch failed for ${input.taskId}; trying the portable processor:`,\n error,\n );\n }\n }\n\n try {\n await fireInternalDispatch({\n event: input.event,\n baseUrl: input.baseUrl,\n path: INTEGRATION_PROCESS_TASK_PATH,\n taskId: input.taskId,\n settleMs: input.portableSettleMs,\n });\n await recordDispatch(input.taskId, \"portable-unconfirmed\");\n logDispatch(input.taskId, \"portable-unconfirmed\", startedAt);\n return \"portable-unconfirmed\";\n } catch (error) {\n console.error(\n `[integrations] Portable dispatch failed for ${input.taskId}:`,\n error,\n );\n await recordDispatch(input.taskId, \"failed\");\n logDispatch(input.taskId, \"failed\", startedAt);\n return \"failed\";\n }\n}\n"]}
@@ -1,8 +1,20 @@
1
+ export interface PendingTasksSweepResult {
2
+ selected: number;
3
+ dispatched: number;
4
+ markedFailed: number;
5
+ skipped: number;
6
+ dispatchFailed: number;
7
+ }
8
+ export interface PendingTasksSweepOptions {
9
+ webhookBaseUrl?: string;
10
+ limit?: number;
11
+ durableOnly?: boolean;
12
+ }
1
13
  /**
2
14
  * One pass: find stuck tasks and re-fire the processor for each.
3
15
  * Exported for tests and for manual triggers.
4
16
  */
5
- export declare function retryStuckPendingTasks(webhookBaseUrl?: string): Promise<void>;
17
+ export declare function retryStuckPendingTasks(input?: string | PendingTasksSweepOptions): Promise<PendingTasksSweepResult>;
6
18
  /**
7
19
  * Start the periodic retry loop. Safe to call multiple times — second call
8
20
  * is a no-op.
@@ -1 +1 @@
1
- {"version":3,"file":"pending-tasks-retry-job.d.ts","sourceRoot":"","sources":["../../src/integrations/pending-tasks-retry-job.ts"],"names":[],"mappings":"AAuDA;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAiGf;AA8ED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE;IAClD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GAAG,IAAI,CA0BP;AAED,2BAA2B;AAC3B,wBAAgB,wBAAwB,IAAI,IAAI,CAU/C"}
1
+ {"version":3,"file":"pending-tasks-retry-job.d.ts","sourceRoot":"","sources":["../../src/integrations/pending-tasks-retry-job.ts"],"names":[],"mappings":"AAqEA,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAkDD;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,CAAC,EAAE,MAAM,GAAG,wBAAwB,GACxC,OAAO,CAAC,uBAAuB,CAAC,CA6KlC;AAcD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE;IAClD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GAAG,IAAI,CA0BP;AAED,2BAA2B;AAC3B,wBAAgB,wBAAwB,IAAI,IAAI,CAU/C"}
@@ -1,20 +1,22 @@
1
1
  import { getDbExec } from "../db/client.js";
2
- import { withConfiguredAppBasePath } from "../server/app-base-path.js";
3
- import { FRAMEWORK_ROUTE_PREFIX } from "../server/core-routes-plugin.js";
4
- import { signInternalToken } from "./internal-token.js";
5
- import { MAX_PENDING_TASK_ATTEMPTS } from "./pending-tasks-store.js";
2
+ import { configuredIntegrationDurableDispatchScopes, dispatchPendingIntegrationTask, isIntegrationDurableDispatchEnabledForTask, } from "./integration-durable-dispatch.js";
3
+ import { ensurePendingTasksTable, MAX_PENDING_TASK_ATTEMPTS, } from "./pending-tasks-store.js";
6
4
  /**
7
5
  * Retries stuck integration webhook tasks.
8
6
  *
9
7
  * The integration webhook flow enqueues work into `integration_pending_tasks`
10
8
  * (see `pending-tasks-store.ts`) and then fires a self-webhook to the
11
9
  * `/_agent-native/integrations/process-task` endpoint to drain the queue.
12
- * If that fire-and-forget dispatch fails (e.g. transient network blip), the
10
+ * If that initial dispatch is lost (e.g. transient network blip), the
13
11
  * row stays in `pending` forever. Likewise, if the processor is killed mid-
14
12
  * processing (function timeout, container shutdown), a row can remain in
15
13
  * `processing` forever.
16
14
  *
17
- * This job runs every 60s and re-fires the processor endpoint for tasks that
15
+ * The in-process fallback runs every 60s. Durable deployments also invoke the
16
+ * same bounded sweep from an external scheduler so recovery does not depend on
17
+ * a serverless process remaining alive.
18
+ *
19
+ * Each sweep re-fires the processor endpoint for tasks that
18
20
  * look stuck:
19
21
  * - status='pending' AND created_at older than 90s (initial dispatch lost)
20
22
  * - status='processing' AND updated_at older than the host-specific
@@ -33,8 +35,9 @@ const PENDING_STUCK_AFTER_MS = 90_000;
33
35
  /** Tasks "processing" longer than this are considered killed mid-flight. */
34
36
  const DEFAULT_PROCESSING_STUCK_AFTER_MS = 5 * 60 * 1000;
35
37
  const SERVERLESS_PROCESSING_STUCK_AFTER_MS = 75_000;
38
+ const DURABLE_BACKGROUND_PROCESSING_STUCK_AFTER_MS = 16 * 60 * 1000;
36
39
  /** After this many attempts we give up and mark the task failed */
37
- const PROCESSOR_PATH = `${FRAMEWORK_ROUTE_PREFIX}/integrations/process-task`;
40
+ const DEFAULT_SWEEP_LIMIT = 100;
38
41
  let retryInterval = null;
39
42
  let initialTimer = null;
40
43
  let activeWebhookBaseUrl;
@@ -43,58 +46,137 @@ let activeWebhookBaseUrl;
43
46
  * minute when the queue isn't in use yet on a given deployment.
44
47
  */
45
48
  let tableExists = null;
49
+ function affectedRows(result) {
50
+ return Number(result.rowsAffected ??
51
+ result.rowCount ??
52
+ 0);
53
+ }
54
+ function isMissingPendingTasksTableError(error) {
55
+ const message = error instanceof Error ? error.message : String(error ?? "");
56
+ return /no such table|relation .* does not exist|undefined_table/i.test(message);
57
+ }
58
+ function durableScopeSql() {
59
+ const scopes = configuredIntegrationDurableDispatchScopes();
60
+ if (scopes === null)
61
+ return { clause: "", args: [] };
62
+ if (scopes.length === 0)
63
+ return { clause: " AND 1 = 0", args: [] };
64
+ const clauses = [];
65
+ const args = [];
66
+ for (const scope of scopes) {
67
+ if (scope.value === "*") {
68
+ clauses.push("platform = ?");
69
+ args.push(scope.platform);
70
+ continue;
71
+ }
72
+ if (scope.platform === "slack" && /^[A-Za-z0-9]+$/.test(scope.value)) {
73
+ clauses.push("(platform = ? AND (external_thread_id = ? OR dispatch_scope = ? OR external_thread_id LIKE ?))");
74
+ args.push(scope.platform, scope.value, scope.value, `%:%:${scope.value}:%`);
75
+ continue;
76
+ }
77
+ clauses.push("(platform = ? AND (external_thread_id = ? OR dispatch_scope = ?))");
78
+ args.push(scope.platform, scope.value, scope.value);
79
+ }
80
+ return { clause: ` AND (${clauses.join(" OR ")})`, args };
81
+ }
46
82
  /**
47
83
  * One pass: find stuck tasks and re-fire the processor for each.
48
84
  * Exported for tests and for manual triggers.
49
85
  */
50
- export async function retryStuckPendingTasks(webhookBaseUrl) {
51
- const baseUrl = webhookBaseUrl ?? activeWebhookBaseUrl;
86
+ export async function retryStuckPendingTasks(input) {
87
+ const options = typeof input === "string" ? { webhookBaseUrl: input } : (input ?? {});
88
+ const baseUrl = options.webhookBaseUrl ?? activeWebhookBaseUrl;
89
+ const limit = Math.max(1, Math.min(500, Math.floor(options.limit ?? DEFAULT_SWEEP_LIMIT)));
90
+ const scopeSql = options.durableOnly
91
+ ? durableScopeSql()
92
+ : { clause: "", args: [] };
93
+ const result = {
94
+ selected: 0,
95
+ dispatched: 0,
96
+ markedFailed: 0,
97
+ skipped: 0,
98
+ dispatchFailed: 0,
99
+ };
100
+ await ensurePendingTasksTable();
52
101
  const client = getDbExec();
53
102
  const now = Date.now();
54
103
  const pendingCutoff = now - PENDING_STUCK_AFTER_MS;
55
104
  const processingCutoff = now - getProcessingStuckAfterMs();
105
+ const durableProcessingCutoff = now - DURABLE_BACKGROUND_PROCESSING_STUCK_AFTER_MS;
56
106
  let stuckRows;
57
107
  try {
58
108
  const { rows } = await client.execute({
59
109
  sql: `
60
- SELECT id, status, attempts
110
+ SELECT id, platform, external_thread_id, dispatch_scope, status, attempts, updated_at
61
111
  FROM integration_pending_tasks
62
- WHERE (status = 'pending' AND created_at <= ? AND updated_at <= ?)
63
- OR (status = 'processing' AND updated_at <= ?)
112
+ WHERE ((status = 'pending' AND created_at <= ? AND updated_at <= ?)
113
+ OR (status = 'processing' AND (
114
+ (last_dispatch_outcome = 'background-acknowledged'
115
+ AND updated_at <= ?)
116
+ OR ((last_dispatch_outcome IS NULL
117
+ OR last_dispatch_outcome <> 'background-acknowledged')
118
+ AND updated_at <= ?)
119
+ )))
120
+ ${scopeSql.clause}
121
+ ORDER BY updated_at ASC
122
+ LIMIT ?
64
123
  `,
65
124
  // `updated_at` is initialized to `created_at` on insert, so a genuinely
66
125
  // stuck pending row still matches on the first sweep. The retry path
67
126
  // below touches `updated_at`, which (with this predicate) keeps the row
68
127
  // from being re-selected — and re-firing the processor — on every tick.
69
- args: [pendingCutoff, pendingCutoff, processingCutoff],
128
+ args: [
129
+ pendingCutoff,
130
+ pendingCutoff,
131
+ durableProcessingCutoff,
132
+ processingCutoff,
133
+ ...scopeSql.args,
134
+ limit,
135
+ ],
70
136
  });
71
137
  stuckRows = rows.map((r) => ({
72
138
  id: r.id,
139
+ platform: r.platform,
140
+ externalThreadId: r.external_thread_id,
73
141
  status: r.status,
74
142
  attempts: Number(r.attempts ?? 0),
143
+ updatedAt: Number(r.updated_at ?? 0),
144
+ dispatchScope: r.dispatch_scope ?? null,
75
145
  }));
76
146
  tableExists = true;
77
147
  }
78
148
  catch (err) {
79
- // Most common case: the table hasn't been created yet because no inbound
80
- // integration webhook has been processed on this deployment. Silently
81
- // no-op until the table appears.
149
+ if (!isMissingPendingTasksTableError(err))
150
+ throw err;
82
151
  if (tableExists !== false) {
83
152
  tableExists = false;
84
153
  if (process.env.DEBUG) {
85
154
  console.log("[integrations] pending-tasks retry job: table not present yet, skipping");
86
155
  }
87
156
  }
88
- return;
157
+ return result;
89
158
  }
159
+ stuckRows = stuckRows.filter((row) => {
160
+ const durable = isIntegrationDurableDispatchEnabledForTask({
161
+ platform: row.platform,
162
+ externalThreadId: row.externalThreadId,
163
+ platformContext: row.dispatchScope
164
+ ? { channelId: row.dispatchScope }
165
+ : undefined,
166
+ });
167
+ if (options.durableOnly && !durable)
168
+ return false;
169
+ return true;
170
+ });
171
+ result.selected = stuckRows.length;
90
172
  if (stuckRows.length === 0)
91
- return;
173
+ return result;
92
174
  for (const row of stuckRows) {
93
175
  try {
94
176
  // Cap retries — mark failed and move on so the row stops bouncing
95
177
  // between pending and processing forever.
96
178
  if (row.attempts >= MAX_PENDING_TASK_ATTEMPTS) {
97
- await client.execute({
179
+ const update = await client.execute({
98
180
  sql: `
99
181
  UPDATE integration_pending_tasks
100
182
  SET status = 'failed',
@@ -104,14 +186,21 @@ export async function retryStuckPendingTasks(webhookBaseUrl) {
104
186
  external_event_key = NULL
105
187
  WHERE id = ?
106
188
  AND status = ?
189
+ AND updated_at = ?
107
190
  `,
108
191
  args: [
109
192
  Date.now(),
110
193
  `Retry job: exceeded ${MAX_PENDING_TASK_ATTEMPTS} attempts`,
111
194
  row.id,
112
195
  row.status,
196
+ row.updatedAt,
113
197
  ],
114
198
  });
199
+ if (affectedRows(update) === 0) {
200
+ result.skipped += 1;
201
+ continue;
202
+ }
203
+ result.markedFailed += 1;
115
204
  console.warn(`[integrations] Pending task ${row.id} exceeded ${MAX_PENDING_TASK_ATTEMPTS} attempts — marking failed`);
116
205
  continue;
117
206
  }
@@ -120,21 +209,43 @@ export async function retryStuckPendingTasks(webhookBaseUrl) {
120
209
  // Without this, processing rows stay stuck forever.
121
210
  // For pending rows, just touch updated_at to avoid re-firing every tick.
122
211
  const newStatus = row.status === "processing" ? "pending" : row.status;
123
- await client.execute({
212
+ const update = await client.execute({
124
213
  sql: `
125
214
  UPDATE integration_pending_tasks
126
215
  SET status = ?, updated_at = ?
127
216
  WHERE id = ?
128
217
  AND status = ?
218
+ AND updated_at = ?
129
219
  `,
130
- args: [newStatus, Date.now(), row.id, row.status],
220
+ args: [newStatus, Date.now(), row.id, row.status, row.updatedAt],
221
+ });
222
+ if (affectedRows(update) === 0) {
223
+ result.skipped += 1;
224
+ continue;
225
+ }
226
+ const outcome = await dispatchPendingIntegrationTask({
227
+ taskId: row.id,
228
+ task: {
229
+ platform: row.platform,
230
+ externalThreadId: row.externalThreadId,
231
+ platformContext: row.dispatchScope
232
+ ? { channelId: row.dispatchScope }
233
+ : undefined,
234
+ },
235
+ baseUrl,
236
+ portableSettleMs: 1_000,
131
237
  });
132
- await refireProcessor(row.id, baseUrl);
238
+ if (outcome === "failed")
239
+ result.dispatchFailed += 1;
240
+ else
241
+ result.dispatched += 1;
133
242
  }
134
243
  catch (err) {
244
+ result.dispatchFailed += 1;
135
245
  console.error(`[integrations] Failed to retry pending task ${row.id}:`, err);
136
246
  }
137
247
  }
248
+ return result;
138
249
  }
139
250
  function getProcessingStuckAfterMs() {
140
251
  if (process.env.NETLIFY ||
@@ -145,58 +256,6 @@ function getProcessingStuckAfterMs() {
145
256
  }
146
257
  return DEFAULT_PROCESSING_STUCK_AFTER_MS;
147
258
  }
148
- /**
149
- * Fire-and-forget POST to the processor endpoint for a single task id.
150
- * Mirrors the original dispatch from the webhook handler, including the
151
- * short-lived HMAC bearer token bound to this taskId.
152
- */
153
- async function refireProcessor(taskId, webhookBaseUrl) {
154
- const baseUrl = webhookBaseUrl ||
155
- process.env.WEBHOOK_BASE_URL ||
156
- process.env.APP_URL ||
157
- process.env.URL ||
158
- process.env.DEPLOY_URL ||
159
- `http://localhost:${process.env.PORT || 3000}`;
160
- const url = `${withConfiguredAppBasePath(baseUrl)}${PROCESSOR_PATH}`;
161
- // Sign with HMAC if A2A_SECRET is configured. In production we MUST sign —
162
- // an unsigned dispatch in production lets attackers re-trigger any queued
163
- // task with a guessable id (C3 in the webhook security audit). In dev we
164
- // fall back to unsigned so contributors can iterate without configuring
165
- // A2A_SECRET locally.
166
- const headers = {
167
- "Content-Type": "application/json",
168
- };
169
- try {
170
- headers["Authorization"] = `Bearer ${signInternalToken(taskId)}`;
171
- }
172
- catch (err) {
173
- if (process.env.NODE_ENV === "production") {
174
- console.error(`[integrations] Refusing to dispatch task ${taskId} — A2A_SECRET not configured. ` +
175
- "Set A2A_SECRET to enable signed retry dispatches.");
176
- return;
177
- }
178
- // Dev: proceed unsigned. Log the underlying error path so a malformed
179
- // secret (different from "not set") doesn't fail silently (L5 in the audit).
180
- if (err instanceof Error && !/A2A_SECRET/i.test(err.message)) {
181
- console.error(`[integrations] signInternalToken failed unexpectedly for ${taskId}:`, err);
182
- }
183
- }
184
- // Don't await the body — we just want the request to leave the box.
185
- // A short timeout avoids tying up the retry loop on a hung processor.
186
- const controller = new AbortController();
187
- const timer = setTimeout(() => controller.abort(), 5_000);
188
- try {
189
- await fetch(url, {
190
- method: "POST",
191
- headers,
192
- body: JSON.stringify({ taskId }),
193
- signal: controller.signal,
194
- });
195
- }
196
- finally {
197
- clearTimeout(timer);
198
- }
199
- }
200
259
  /**
201
260
  * Start the periodic retry loop. Safe to call multiple times — second call
202
261
  * is a no-op.