@agent-native/core 0.119.4 → 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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/durable-background.ts +1 -0
- package/corpus/core/src/agent/engine/index.ts +2 -0
- package/corpus/core/src/agent/engine/registry.ts +95 -2
- package/corpus/core/src/deploy/build.ts +144 -2
- package/corpus/core/src/deploy/workspace-deploy.ts +123 -1
- package/corpus/core/src/integrations/integration-durable-dispatch-config.ts +22 -0
- package/corpus/core/src/integrations/integration-durable-dispatch.ts +190 -0
- package/corpus/core/src/integrations/pending-tasks-retry-job.ts +175 -87
- package/corpus/core/src/integrations/pending-tasks-store.ts +114 -11
- package/corpus/core/src/integrations/plugin.ts +113 -60
- package/corpus/core/src/integrations/task-queue-stats.ts +68 -15
- package/corpus/core/src/integrations/webhook-handler.ts +21 -46
- package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +8 -0
- package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +5 -1
- package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +9 -1
- package/corpus/core/src/server/auth.ts +6 -0
- package/corpus/core/src/templates/default/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/corpus/core/src/templates/headless/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/corpus/core/src/templates/workspace-core/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/corpus/templates/dispatch/changelog/2026-07-21-recover-stalled-slack-agent-work.md +6 -0
- package/corpus/templates/slides/actions/patch-deck.ts +2 -1
- package/corpus/templates/slides/changelog/2026-07-23-opening-or-refining-a-deck-no-longer-crashes-with-a-blank-er.md +6 -0
- package/corpus/templates/slides/shared/aspect-ratios.ts +4 -1
- package/corpus/templates/tasks/.oxfmtrc.json +8 -0
- package/corpus/templates/tasks/AGENTS.md +34 -41
- package/corpus/templates/tasks/README.md +13 -6
- package/corpus/templates/tasks/actions/bulk-delete-inbox-items.ts +10 -8
- package/corpus/templates/tasks/actions/bulk-delete-tasks.ts +10 -8
- package/corpus/templates/tasks/actions/bulk-mark-inbox-items-ready.ts +1 -1
- package/corpus/templates/tasks/actions/bulk-update-tasks.ts +12 -10
- package/corpus/templates/tasks/actions/create-inbox-item.ts +7 -4
- package/corpus/templates/tasks/actions/create-task.ts +2 -1
- package/corpus/templates/tasks/actions/delete-inbox-item.ts +6 -4
- package/corpus/templates/tasks/actions/delete-task.ts +1 -1
- package/corpus/templates/tasks/actions/list-inbox-items.ts +4 -2
- package/corpus/templates/tasks/actions/list-visible-task-fields.ts +3 -1
- package/corpus/templates/tasks/actions/mark-inbox-item-ready.ts +6 -4
- package/corpus/templates/tasks/actions/navigate.ts +16 -14
- package/corpus/templates/tasks/actions/reorder-inbox-items.ts +9 -7
- package/corpus/templates/tasks/actions/update-inbox-item.ts +6 -4
- package/corpus/templates/tasks/actions/update-task.ts +12 -9
- package/corpus/templates/tasks/actions/update-visible-task-fields.ts +10 -8
- package/corpus/templates/tasks/actions/view-screen.ts +1 -1
- package/corpus/templates/tasks/app/components/tasks/fields/TaskFieldsSidebar.tsx +15 -0
- package/corpus/templates/tasks/changelog/2026-07-23-task-detail-extensions.md +6 -0
- package/corpus/templates/tasks/drizzle.config.ts +3 -0
- package/corpus/templates/tasks/package.json +4 -1
- package/corpus/templates/tasks/server/db/schema.ts +0 -10
- package/corpus/templates/tasks/server/db/test-tasks-table.ts +0 -5
- package/corpus/templates/tasks/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/tasks/server/plugins/db.ts +43 -10
- package/corpus/templates/tasks/server/user-config/store.ts +42 -81
- package/corpus/templates/tasks/tsconfig.json +1 -2
- package/corpus/templates/tasks/vitest.config.ts +10 -1
- package/dist/agent/durable-background.d.ts +1 -0
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +1 -0
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/engine/index.d.ts +1 -1
- package/dist/agent/engine/index.d.ts.map +1 -1
- package/dist/agent/engine/index.js +1 -1
- package/dist/agent/engine/index.js.map +1 -1
- package/dist/agent/engine/registry.d.ts +32 -1
- package/dist/agent/engine/registry.d.ts.map +1 -1
- package/dist/agent/engine/registry.js +75 -3
- package/dist/agent/engine/registry.js.map +1 -1
- package/dist/deploy/build.d.ts +3 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +106 -3
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/workspace-deploy.d.ts.map +1 -1
- package/dist/deploy/workspace-deploy.js +103 -2
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/integration-durable-dispatch-config.d.ts +7 -0
- package/dist/integrations/integration-durable-dispatch-config.d.ts.map +1 -0
- package/dist/integrations/integration-durable-dispatch-config.js +16 -0
- package/dist/integrations/integration-durable-dispatch-config.js.map +1 -0
- package/dist/integrations/integration-durable-dispatch.d.ts +23 -0
- package/dist/integrations/integration-durable-dispatch.d.ts.map +1 -0
- package/dist/integrations/integration-durable-dispatch.js +115 -0
- package/dist/integrations/integration-durable-dispatch.js.map +1 -0
- package/dist/integrations/pending-tasks-retry-job.d.ts +13 -1
- package/dist/integrations/pending-tasks-retry-job.d.ts.map +1 -1
- package/dist/integrations/pending-tasks-retry-job.js +133 -74
- package/dist/integrations/pending-tasks-retry-job.js.map +1 -1
- package/dist/integrations/pending-tasks-store.d.ts +14 -2
- package/dist/integrations/pending-tasks-store.d.ts.map +1 -1
- package/dist/integrations/pending-tasks-store.js +75 -11
- package/dist/integrations/pending-tasks-store.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +88 -55
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/task-queue-stats.d.ts +15 -3
- package/dist/integrations/task-queue-stats.d.ts.map +1 -1
- package/dist/integrations/task-queue-stats.js +45 -15
- package/dist/integrations/task-queue-stats.js.map +1 -1
- package/dist/integrations/webhook-handler.d.ts +1 -1
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +18 -42
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
- package/dist/scripts/agent-engines/list-agent-engines.js +8 -2
- package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
- package/dist/scripts/agent-engines/manage-agent-engine.d.ts.map +1 -1
- package/dist/scripts/agent-engines/manage-agent-engine.js +5 -2
- package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
- package/dist/scripts/agent-engines/set-agent-engine.d.ts.map +1 -1
- package/dist/scripts/agent-engines/set-agent-engine.js +9 -2
- package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +5 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/default/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/dist/templates/headless/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/dist/templates/workspace-core/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/package.json +1 -1
- package/src/agent/durable-background.ts +1 -0
- package/src/agent/engine/index.ts +2 -0
- package/src/agent/engine/registry.ts +95 -2
- package/src/deploy/build.ts +144 -2
- package/src/deploy/workspace-deploy.ts +123 -1
- package/src/integrations/integration-durable-dispatch-config.ts +22 -0
- package/src/integrations/integration-durable-dispatch.ts +190 -0
- package/src/integrations/pending-tasks-retry-job.ts +175 -87
- package/src/integrations/pending-tasks-store.ts +114 -11
- package/src/integrations/plugin.ts +113 -60
- package/src/integrations/task-queue-stats.ts +68 -15
- package/src/integrations/webhook-handler.ts +21 -46
- package/src/scripts/agent-engines/list-agent-engines.ts +8 -0
- package/src/scripts/agent-engines/manage-agent-engine.ts +5 -1
- package/src/scripts/agent-engines/set-agent-engine.ts +9 -1
- package/src/server/auth.ts +6 -0
- package/src/templates/default/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/src/templates/headless/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/src/templates/workspace-core/.agents/skills/integration-webhooks/SKILL.md +63 -22
- package/corpus/templates/tasks/.prettierrc +0 -5
|
@@ -46,7 +46,6 @@ import { updateThreadData } from "../chat-threads/store.js";
|
|
|
46
46
|
import { isLocalDatabase } from "../db/client.js";
|
|
47
47
|
import { getOrgA2ASecret, resolveOrgIdForEmail } from "../org/context.js";
|
|
48
48
|
import { withConfiguredAppBasePath } from "../server/app-base-path.js";
|
|
49
|
-
import { FRAMEWORK_ROUTE_PREFIX } from "../server/core-routes-plugin.js";
|
|
50
49
|
import {
|
|
51
50
|
canUseDeployCredentialFallbackForRequest,
|
|
52
51
|
readDeployCredentialEnv,
|
|
@@ -58,8 +57,11 @@ import {
|
|
|
58
57
|
clearIntegrationAwaitingInput,
|
|
59
58
|
setIntegrationAwaitingInput,
|
|
60
59
|
} from "./awaiting-input-store.js";
|
|
60
|
+
import {
|
|
61
|
+
dispatchPendingIntegrationTask,
|
|
62
|
+
integrationDispatchScopeValue,
|
|
63
|
+
} from "./integration-durable-dispatch.js";
|
|
61
64
|
import { loadIntegrationMemoryPrompt } from "./integration-memory.js";
|
|
62
|
-
import { signInternalToken } from "./internal-token.js";
|
|
63
65
|
import {
|
|
64
66
|
insertPendingTask,
|
|
65
67
|
isDuplicateEventError,
|
|
@@ -307,7 +309,7 @@ export async function resolveIntegrationApiKey(
|
|
|
307
309
|
* 2. Verify webhook signature
|
|
308
310
|
* 3. Parse incoming message (null = ignored event)
|
|
309
311
|
* 4. Persist task to SQL
|
|
310
|
-
* 5.
|
|
312
|
+
* 5. Dispatch the queued task through the configured processor handoff
|
|
311
313
|
* (a fresh function execution with its own timeout budget)
|
|
312
314
|
* 6. Return HTTP 200 immediately (within Slack's 3s SLA)
|
|
313
315
|
*
|
|
@@ -477,48 +479,14 @@ async function enqueueAndDispatch(
|
|
|
477
479
|
// platform produce the same key, so the unique index rejects the
|
|
478
480
|
// second insert (H3 in the webhook security audit).
|
|
479
481
|
externalEventKey: buildEventDedupKey(incoming),
|
|
482
|
+
dispatchScope: integrationDispatchScopeValue({
|
|
483
|
+
platform: incoming.platform,
|
|
484
|
+
externalThreadId: incoming.externalThreadId,
|
|
485
|
+
platformContext: incoming.platformContext,
|
|
486
|
+
}),
|
|
480
487
|
});
|
|
481
488
|
|
|
482
489
|
const baseUrl = resolveBaseUrl(event);
|
|
483
|
-
const processUrl = `${baseUrl}${FRAMEWORK_ROUTE_PREFIX}/integrations/process-task`;
|
|
484
|
-
|
|
485
|
-
// Sign the dispatch with an HMAC token so the processor endpoint can
|
|
486
|
-
// verify the request came from us and not the public internet. The
|
|
487
|
-
// processor refuses unsigned requests in production (C3 in the webhook
|
|
488
|
-
// security audit). In dev, dispatching unsigned is allowed and falls
|
|
489
|
-
// through to the SQL atomic claim for double-processing protection.
|
|
490
|
-
const headers: Record<string, string> = {
|
|
491
|
-
"Content-Type": "application/json",
|
|
492
|
-
};
|
|
493
|
-
try {
|
|
494
|
-
headers["Authorization"] = `Bearer ${signInternalToken(taskId)}`;
|
|
495
|
-
} catch (err) {
|
|
496
|
-
// Distinguish "secret not configured" (the documented dev path) from
|
|
497
|
-
// a real signing failure — silently swallowing both made malformed
|
|
498
|
-
// secrets fail invisibly (L5 in the audit).
|
|
499
|
-
if (err instanceof Error && !/A2A_SECRET/i.test(err.message)) {
|
|
500
|
-
console.error(
|
|
501
|
-
`[integrations] signInternalToken failed unexpectedly for ${taskId}:`,
|
|
502
|
-
err,
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Fire-and-forget: do NOT await the full response (the processor's run
|
|
508
|
-
// takes minutes — we don't want to block the caller). BUT on Netlify
|
|
509
|
-
// Lambda, when we return immediately, the runtime can freeze the function
|
|
510
|
-
// before the outbound TCP handshake even starts, which leaves the dispatch
|
|
511
|
-
// request stuck waiting for the 60s retry-sweep job. Race the fetch
|
|
512
|
-
// against a short timer so the request gets a reasonable chance to leave
|
|
513
|
-
// the box; the trade-off is at most a couple seconds of added webhook
|
|
514
|
-
// latency, still inside Slack's timeout window.
|
|
515
|
-
const dispatchPromise = fetch(processUrl, {
|
|
516
|
-
method: "POST",
|
|
517
|
-
headers,
|
|
518
|
-
body: JSON.stringify({ taskId }),
|
|
519
|
-
}).catch((err) => {
|
|
520
|
-
console.error("[integrations] Failed to dispatch processor request:", err);
|
|
521
|
-
});
|
|
522
490
|
const settleWaitMs = options.adapter.capabilities?.deferredWebhookResponse
|
|
523
491
|
? Math.min(
|
|
524
492
|
DEFERRED_RESPONSE_DISPATCH_SETTLE_WAIT_MS,
|
|
@@ -528,10 +496,17 @@ async function enqueueAndDispatch(
|
|
|
528
496
|
),
|
|
529
497
|
)
|
|
530
498
|
: PROCESSOR_DISPATCH_SETTLE_WAIT_MS;
|
|
531
|
-
await
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
499
|
+
await dispatchPendingIntegrationTask({
|
|
500
|
+
taskId,
|
|
501
|
+
task: {
|
|
502
|
+
platform: incoming.platform,
|
|
503
|
+
externalThreadId: incoming.externalThreadId,
|
|
504
|
+
platformContext: incoming.platformContext,
|
|
505
|
+
},
|
|
506
|
+
event,
|
|
507
|
+
baseUrl,
|
|
508
|
+
portableSettleMs: settleWaitMs,
|
|
509
|
+
});
|
|
535
510
|
}
|
|
536
511
|
|
|
537
512
|
/**
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
isAgentEnginePackageInstalled,
|
|
14
14
|
isStoredEngineUsableForRequest,
|
|
15
15
|
normalizeModelForEngine,
|
|
16
|
+
resolveEnginePreservesCustomModels,
|
|
16
17
|
} from "../../agent/engine/index.js";
|
|
17
18
|
import type { ActionTool } from "../../agent/types.js";
|
|
18
19
|
import { getSetting } from "../../settings/index.js";
|
|
@@ -82,11 +83,18 @@ export async function run(args: Record<string, string> = {}): Promise<string> {
|
|
|
82
83
|
? current?.model
|
|
83
84
|
: undefined;
|
|
84
85
|
const currentEngineName = currentEntry?.name ?? "anthropic";
|
|
86
|
+
// Resolve the OpenAI-compatible-endpoint capability so a custom gateway model
|
|
87
|
+
// is reported as-is instead of being normalized to the engine default — the
|
|
88
|
+
// read-side counterpart of the same fix in set-/manage-agent-engine.
|
|
89
|
+
const preserveCustomModels = currentEntry
|
|
90
|
+
? await resolveEnginePreservesCustomModels(currentEntry)
|
|
91
|
+
: false;
|
|
85
92
|
const currentModel =
|
|
86
93
|
currentEntry && !envUnavailable
|
|
87
94
|
? normalizeModelForEngine(
|
|
88
95
|
currentEntry,
|
|
89
96
|
currentModelCandidate ?? currentEntry.defaultModel,
|
|
97
|
+
{ preserveCustomModels },
|
|
90
98
|
)
|
|
91
99
|
: (currentModelCandidate ?? DEFAULT_MODEL);
|
|
92
100
|
const result = {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
getAgentEngineEntry,
|
|
17
17
|
isAgentEnginePackageInstalled,
|
|
18
18
|
normalizeModelForEngine,
|
|
19
|
+
resolveEnginePreservesCustomModels,
|
|
19
20
|
registerBuiltinEngines,
|
|
20
21
|
} from "../../agent/engine/index.js";
|
|
21
22
|
import type { ActionTool } from "../../agent/types.js";
|
|
@@ -113,7 +114,10 @@ async function runSetAppDefault(args: Record<string, string>): Promise<string> {
|
|
|
113
114
|
if (!isAgentEnginePackageInstalled(entry)) {
|
|
114
115
|
return `Error: Engine "${engine}" requires optional packages that are not installed in this app. Run: pnpm add ${entry.installPackage}`;
|
|
115
116
|
}
|
|
116
|
-
const
|
|
117
|
+
const preserveCustomModels = await resolveEnginePreservesCustomModels(entry);
|
|
118
|
+
const normalizedModel = normalizeModelForEngine(entry, model, {
|
|
119
|
+
preserveCustomModels,
|
|
120
|
+
});
|
|
117
121
|
|
|
118
122
|
const ctx = currentContext();
|
|
119
123
|
const canUpdate = await canUpdateAgentAppModelDefaultSettings(
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isAgentEnginePackageInstalled,
|
|
9
9
|
isStoredEngineUsableForRequest,
|
|
10
10
|
normalizeModelForEngine,
|
|
11
|
+
resolveEnginePreservesCustomModels,
|
|
11
12
|
registerBuiltinEngines,
|
|
12
13
|
} from "../../agent/engine/index.js";
|
|
13
14
|
import type { ActionTool } from "../../agent/types.js";
|
|
@@ -54,7 +55,14 @@ export async function run(args: Record<string, string>): Promise<string> {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
const requestedModel = model ?? entry.defaultModel;
|
|
57
|
-
|
|
58
|
+
// A static registry entry can't carry the runtime `preserveCustomModels`
|
|
59
|
+
// flag, so resolve the OpenAI-compatible-endpoint capability here and pass it
|
|
60
|
+
// through — otherwise a gateway model (e.g. an Ollama id) is rewritten to the
|
|
61
|
+
// engine default on save.
|
|
62
|
+
const preserveCustomModels = await resolveEnginePreservesCustomModels(entry);
|
|
63
|
+
const resolvedModel = normalizeModelForEngine(entry, requestedModel, {
|
|
64
|
+
preserveCustomModels,
|
|
65
|
+
});
|
|
58
66
|
|
|
59
67
|
const usable = await isStoredEngineUsableForRequest(
|
|
60
68
|
{ engine: engineName },
|
|
@@ -1740,6 +1740,12 @@ function createAuthGuardFn(): (
|
|
|
1740
1740
|
return;
|
|
1741
1741
|
}
|
|
1742
1742
|
|
|
1743
|
+
// External durable-recovery scheduler. The route verifies a short-lived
|
|
1744
|
+
// HMAC token bound to its fixed sweep subject before touching the queue.
|
|
1745
|
+
if (p === "/_agent-native/integrations/retry-stuck-tasks") {
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1743
1749
|
// Internal processor endpoint for deferred A2A continuations created by
|
|
1744
1750
|
// integration tasks. It uses the same HMAC internal-token scheme as the
|
|
1745
1751
|
// primary integration processor, so it must bypass cookie/session auth.
|
|
@@ -16,10 +16,10 @@ metadata:
|
|
|
16
16
|
|
|
17
17
|
Integration webhooks (Slack, Telegram, WhatsApp, email, Google Docs, etc.) must
|
|
18
18
|
**enqueue work to SQL and return 200 immediately**, then process the work in a
|
|
19
|
-
**separate fresh function execution** kicked off by a self-fired HTTP POST.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
**separate fresh function execution** kicked off by a self-fired HTTP POST. SQL
|
|
20
|
+
is always the source of truth. Long-lived hosts use an in-process retry loop as
|
|
21
|
+
a best-effort safety net; serverless deployments must use an external durable
|
|
22
|
+
sweep when recovery cannot depend on a process staying alive.
|
|
23
23
|
|
|
24
24
|
Do not run agent loops inside the webhook handler itself. Do not rely on
|
|
25
25
|
fire-and-forget `Promise`s after `return`ing from a serverless handler — they get
|
|
@@ -38,8 +38,9 @@ Past attempts that don't work cross-host:
|
|
|
38
38
|
freeze the execution context the moment the response goes out. The promise
|
|
39
39
|
is silently killed, the user gets no reply, and there's no error in the
|
|
40
40
|
logs.
|
|
41
|
-
- **
|
|
42
|
-
|
|
41
|
+
- **A platform background function as the queue** — host-specific execution is
|
|
42
|
+
useful as a delivery mechanism, but it cannot replace the SQL row or atomic
|
|
43
|
+
claim shared by every host.
|
|
43
44
|
- **Cloudflare `event.waitUntil()`** — CF Workers only, not portable.
|
|
44
45
|
- **Vercel Fluid / `after()`** — Vercel-only, gated behind specific runtimes.
|
|
45
46
|
- **A long-lived in-process queue** — fine on a single Node box, but on
|
|
@@ -62,8 +63,9 @@ and a recurring job is the safety net.
|
|
|
62
63
|
2. INSERT INTO integration_pending_tasks
|
|
63
64
|
(status='pending', payload=...)
|
|
64
65
|
│
|
|
65
|
-
3.
|
|
66
|
-
— fire
|
|
66
|
+
3. dispatch POST /integrations/process-task
|
|
67
|
+
— portable self-fire by default
|
|
68
|
+
— acknowledged background handoff when enabled
|
|
67
69
|
│
|
|
68
70
|
4. return 200 to platform ◄───────────┘
|
|
69
71
|
|
|
@@ -79,7 +81,7 @@ and a recurring job is the safety net.
|
|
|
79
81
|
|
|
80
82
|
|
|
81
83
|
┌──────────────────────────────────────────────┐
|
|
82
|
-
│
|
|
84
|
+
│ Recovery sweep (every 60s) — safety net │
|
|
83
85
|
│ Re-fires processor for tasks stuck in │
|
|
84
86
|
│ 'pending' or 'processing' beyond timeout. │
|
|
85
87
|
│ Caps retries at 3 then marks 'failed'. │
|
|
@@ -98,6 +100,7 @@ loop.
|
|
|
98
100
|
| `packages/core/src/integrations/webhook-handler.ts` | Verifies signature, parses, enqueues task, fires processor |
|
|
99
101
|
| `packages/core/src/integrations/pending-tasks-store.ts` | SQL queue: `insertPendingTask`, `claimPendingTask`, `markTaskCompleted`, `markTaskFailed` |
|
|
100
102
|
| `packages/core/src/integrations/pending-tasks-retry-job.ts` | Recurring retry sweep (`startPendingTasksRetryJob`, `retryStuckPendingTasks`) |
|
|
103
|
+
| `packages/core/src/integrations/integration-durable-dispatch.ts` | Default-off acknowledged dispatch, scoped rollout, and outcome recording |
|
|
101
104
|
| `packages/core/src/integrations/types.ts` | `PlatformAdapter`, `IncomingMessage`, `OutgoingMessage` |
|
|
102
105
|
| `packages/core/src/integrations/adapters/{slack,telegram,whatsapp,email,google-docs}.ts` | One adapter per platform |
|
|
103
106
|
|
|
@@ -108,7 +111,8 @@ All under `/_agent-native/integrations/`:
|
|
|
108
111
|
| Method | Path | Purpose |
|
|
109
112
|
| ------ | -------------------------- | ------------------------------------------------------------- |
|
|
110
113
|
| POST | `/:platform/webhook` | Platform pings this. Verifies, enqueues, returns 200 quickly. |
|
|
111
|
-
| POST | `/
|
|
114
|
+
| POST | `/process-task` | Processor target. Claims a task and runs the agent loop. |
|
|
115
|
+
| POST | `/retry-stuck-tasks` | Signed, bounded recovery sweep for durable schedulers. |
|
|
112
116
|
| GET | `/status` | All integrations status (settings UI). |
|
|
113
117
|
| GET | `/:platform/status` | One platform's status. |
|
|
114
118
|
| POST | `/:platform/enable` | Enable an integration. |
|
|
@@ -129,6 +133,10 @@ CREATE TABLE IF NOT EXISTS integration_pending_tasks (
|
|
|
129
133
|
org_id TEXT,
|
|
130
134
|
status TEXT NOT NULL, -- pending | processing | completed | failed
|
|
131
135
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
136
|
+
dispatch_attempts INTEGER NOT NULL DEFAULT 0,
|
|
137
|
+
last_dispatch_at INTEGER,
|
|
138
|
+
last_dispatch_outcome TEXT,
|
|
139
|
+
dispatch_scope TEXT, -- persisted channel scope for recovery
|
|
132
140
|
error_message TEXT,
|
|
133
141
|
created_at INTEGER NOT NULL,
|
|
134
142
|
updated_at INTEGER NOT NULL,
|
|
@@ -136,6 +144,8 @@ CREATE TABLE IF NOT EXISTS integration_pending_tasks (
|
|
|
136
144
|
);
|
|
137
145
|
CREATE INDEX IF NOT EXISTS idx_pending_tasks_status_created
|
|
138
146
|
ON integration_pending_tasks(status, created_at);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_pending_tasks_dispatch_scope
|
|
148
|
+
ON integration_pending_tasks(platform, dispatch_scope);
|
|
139
149
|
```
|
|
140
150
|
|
|
141
151
|
The store layer creates this lazily on first use via `ensureTable()` and uses
|
|
@@ -143,10 +153,38 @@ The store layer creates this lazily on first use via `ensureTable()` and uses
|
|
|
143
153
|
|
|
144
154
|
`claimPendingTask` is the critical concurrency primitive: it atomically flips
|
|
145
155
|
`pending` → `processing` and increments `attempts`, returning `null` if another
|
|
146
|
-
worker beat us to it. Both the initial
|
|
156
|
+
worker beat us to it. Both the initial dispatch and every recovery sweep
|
|
147
157
|
funnel through the same processor endpoint, and `claimPendingTask` is what
|
|
148
158
|
prevents the same task from being processed twice.
|
|
149
159
|
|
|
160
|
+
### Netlify durable recovery
|
|
161
|
+
|
|
162
|
+
The portable self-dispatch remains the default. To emit and use Netlify's
|
|
163
|
+
acknowledged background worker plus a one-minute scheduled recovery function,
|
|
164
|
+
set `AGENT_INTEGRATION_DURABLE_DISPATCH=true` at build and runtime. A production
|
|
165
|
+
rollout can be narrowed with the comma-separated
|
|
166
|
+
`AGENT_INTEGRATION_DURABLE_DISPATCH_SCOPES` allowlist. Supported values are
|
|
167
|
+
`<platform>:*`, `<platform>:<external-thread-id>`, and, for adapters that expose
|
|
168
|
+
one, `<platform>:<channel-id>` (for example `slack:C123`).
|
|
169
|
+
Channel-scoped handoffs persist that scope separately from the provider thread
|
|
170
|
+
identity so a later bounded recovery sweep applies the same allowlist decision.
|
|
171
|
+
|
|
172
|
+
Workspace deploys emit this pair only for the Dispatch control-plane app, which
|
|
173
|
+
owns workspace messaging integrations. Standalone templates emit their own
|
|
174
|
+
pair when the same flag is enabled.
|
|
175
|
+
|
|
176
|
+
The background handoff waits only for Netlify's enqueue acknowledgement, not
|
|
177
|
+
for the agent loop. A failed or non-2xx acknowledgement falls back to the
|
|
178
|
+
portable processor. The scheduled function calls the HMAC-authenticated,
|
|
179
|
+
bounded `/retry-stuck-tasks` route; its atomic compare-and-set update prevents
|
|
180
|
+
overlapping sweeps from dispatching the same stale row twice.
|
|
181
|
+
Rows acknowledged by the 15-minute background worker use a 16-minute stale
|
|
182
|
+
cutoff, while portable synchronous dispatches retain the shorter serverless
|
|
183
|
+
cutoff. Apply that lease predicate before `LIMIT` so healthy long runs cannot
|
|
184
|
+
hide recoverable work. The background processor writes its lease marker in the
|
|
185
|
+
same atomic update that claims the row; best-effort dispatch telemetry is not
|
|
186
|
+
trusted for replay safety.
|
|
187
|
+
|
|
150
188
|
## Adding a New Platform Adapter
|
|
151
189
|
|
|
152
190
|
1. **Implement `PlatformAdapter`** in `packages/core/src/integrations/adapters/<platform>.ts`:
|
|
@@ -245,9 +283,10 @@ so a normal long-running reply is safe.
|
|
|
245
283
|
|
|
246
284
|
## Cross-Platform Considerations
|
|
247
285
|
|
|
248
|
-
- **
|
|
249
|
-
|
|
250
|
-
|
|
286
|
+
- **Portable correctness, optional host acceleration.** Every host uses SQL,
|
|
287
|
+
HMAC-authenticated processor routes, and atomic claims. A host-specific
|
|
288
|
+
background function or scheduler may provide a more reliable wake-up, but
|
|
289
|
+
disabling it must preserve the portable path.
|
|
251
290
|
- **No assumed runtime.** The processor endpoint is a normal H3 handler under
|
|
252
291
|
`/_agent-native/`. It runs wherever the rest of the framework runs.
|
|
253
292
|
- **No persistent in-memory state.** The dedup map in the webhook handler is
|
|
@@ -262,11 +301,12 @@ so a normal long-running reply is safe.
|
|
|
262
301
|
|
|
263
302
|
## Why Fire-and-Forget on Serverless Is Unreliable
|
|
264
303
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
complete before the function freezes.
|
|
268
|
-
|
|
269
|
-
|
|
304
|
+
The portable webhook path gives the outbound processor request a short head
|
|
305
|
+
start but does not wait for the response body, so that initial dispatch is
|
|
306
|
+
**not** guaranteed to complete before the function freezes. On a long-lived
|
|
307
|
+
host, the in-process retry loop is a best-effort safety net. On Netlify with
|
|
308
|
+
durable dispatch enabled, the initial handoff is acknowledged and the external
|
|
309
|
+
scheduled function supplies the recovery wake-up for cases where:
|
|
270
310
|
|
|
271
311
|
- The serverless platform froze the handler before the outbound `fetch`
|
|
272
312
|
flushed its bytes.
|
|
@@ -278,8 +318,9 @@ Tasks stuck in `pending` for >90s or `processing` for >5min get re-fired up to
|
|
|
278
318
|
3 times. After 3 attempts they're marked `failed` permanently so we stop
|
|
279
319
|
spamming the processor.
|
|
280
320
|
|
|
281
|
-
**Never assume the initial
|
|
282
|
-
|
|
321
|
+
**Never assume the initial dispatch succeeded.** Rely on the SQL queue, atomic
|
|
322
|
+
claim, and a recovery mechanism whose lifetime is independent of the original
|
|
323
|
+
request. An in-process timer alone is not that mechanism on serverless.
|
|
283
324
|
|
|
284
325
|
## Debugging Checklist
|
|
285
326
|
|
|
@@ -292,7 +333,7 @@ queue + retry job for at-least-once delivery.
|
|
|
292
333
|
4. **Status?** `pending` means the processor never picked it up — check that
|
|
293
334
|
`_process-task` is reachable from the box itself (the self-fetch must work
|
|
294
335
|
over the public URL). `processing` for over 5 minutes means the processor
|
|
295
|
-
died mid-run —
|
|
336
|
+
died mid-run — a live recovery sweep will pick it up.
|
|
296
337
|
5. **Failed?** Check `error_message` and `attempts`. After 3 attempts the row
|
|
297
338
|
is parked at `failed` and won't be retried.
|
|
298
339
|
6. **Reply not delivered?** The processor likely succeeded but
|
|
@@ -16,10 +16,10 @@ metadata:
|
|
|
16
16
|
|
|
17
17
|
Integration webhooks (Slack, Telegram, WhatsApp, email, Google Docs, etc.) must
|
|
18
18
|
**enqueue work to SQL and return 200 immediately**, then process the work in a
|
|
19
|
-
**separate fresh function execution** kicked off by a self-fired HTTP POST.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
**separate fresh function execution** kicked off by a self-fired HTTP POST. SQL
|
|
20
|
+
is always the source of truth. Long-lived hosts use an in-process retry loop as
|
|
21
|
+
a best-effort safety net; serverless deployments must use an external durable
|
|
22
|
+
sweep when recovery cannot depend on a process staying alive.
|
|
23
23
|
|
|
24
24
|
Do not run agent loops inside the webhook handler itself. Do not rely on
|
|
25
25
|
fire-and-forget `Promise`s after `return`ing from a serverless handler — they get
|
|
@@ -38,8 +38,9 @@ Past attempts that don't work cross-host:
|
|
|
38
38
|
freeze the execution context the moment the response goes out. The promise
|
|
39
39
|
is silently killed, the user gets no reply, and there's no error in the
|
|
40
40
|
logs.
|
|
41
|
-
- **
|
|
42
|
-
|
|
41
|
+
- **A platform background function as the queue** — host-specific execution is
|
|
42
|
+
useful as a delivery mechanism, but it cannot replace the SQL row or atomic
|
|
43
|
+
claim shared by every host.
|
|
43
44
|
- **Cloudflare `event.waitUntil()`** — CF Workers only, not portable.
|
|
44
45
|
- **Vercel Fluid / `after()`** — Vercel-only, gated behind specific runtimes.
|
|
45
46
|
- **A long-lived in-process queue** — fine on a single Node box, but on
|
|
@@ -62,8 +63,9 @@ and a recurring job is the safety net.
|
|
|
62
63
|
2. INSERT INTO integration_pending_tasks
|
|
63
64
|
(status='pending', payload=...)
|
|
64
65
|
│
|
|
65
|
-
3.
|
|
66
|
-
— fire
|
|
66
|
+
3. dispatch POST /integrations/process-task
|
|
67
|
+
— portable self-fire by default
|
|
68
|
+
— acknowledged background handoff when enabled
|
|
67
69
|
│
|
|
68
70
|
4. return 200 to platform ◄───────────┘
|
|
69
71
|
|
|
@@ -79,7 +81,7 @@ and a recurring job is the safety net.
|
|
|
79
81
|
|
|
80
82
|
|
|
81
83
|
┌──────────────────────────────────────────────┐
|
|
82
|
-
│
|
|
84
|
+
│ Recovery sweep (every 60s) — safety net │
|
|
83
85
|
│ Re-fires processor for tasks stuck in │
|
|
84
86
|
│ 'pending' or 'processing' beyond timeout. │
|
|
85
87
|
│ Caps retries at 3 then marks 'failed'. │
|
|
@@ -98,6 +100,7 @@ loop.
|
|
|
98
100
|
| `packages/core/src/integrations/webhook-handler.ts` | Verifies signature, parses, enqueues task, fires processor |
|
|
99
101
|
| `packages/core/src/integrations/pending-tasks-store.ts` | SQL queue: `insertPendingTask`, `claimPendingTask`, `markTaskCompleted`, `markTaskFailed` |
|
|
100
102
|
| `packages/core/src/integrations/pending-tasks-retry-job.ts` | Recurring retry sweep (`startPendingTasksRetryJob`, `retryStuckPendingTasks`) |
|
|
103
|
+
| `packages/core/src/integrations/integration-durable-dispatch.ts` | Default-off acknowledged dispatch, scoped rollout, and outcome recording |
|
|
101
104
|
| `packages/core/src/integrations/types.ts` | `PlatformAdapter`, `IncomingMessage`, `OutgoingMessage` |
|
|
102
105
|
| `packages/core/src/integrations/adapters/{slack,telegram,whatsapp,email,google-docs}.ts` | One adapter per platform |
|
|
103
106
|
|
|
@@ -108,7 +111,8 @@ All under `/_agent-native/integrations/`:
|
|
|
108
111
|
| Method | Path | Purpose |
|
|
109
112
|
| ------ | -------------------------- | ------------------------------------------------------------- |
|
|
110
113
|
| POST | `/:platform/webhook` | Platform pings this. Verifies, enqueues, returns 200 quickly. |
|
|
111
|
-
| POST | `/
|
|
114
|
+
| POST | `/process-task` | Processor target. Claims a task and runs the agent loop. |
|
|
115
|
+
| POST | `/retry-stuck-tasks` | Signed, bounded recovery sweep for durable schedulers. |
|
|
112
116
|
| GET | `/status` | All integrations status (settings UI). |
|
|
113
117
|
| GET | `/:platform/status` | One platform's status. |
|
|
114
118
|
| POST | `/:platform/enable` | Enable an integration. |
|
|
@@ -129,6 +133,10 @@ CREATE TABLE IF NOT EXISTS integration_pending_tasks (
|
|
|
129
133
|
org_id TEXT,
|
|
130
134
|
status TEXT NOT NULL, -- pending | processing | completed | failed
|
|
131
135
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
136
|
+
dispatch_attempts INTEGER NOT NULL DEFAULT 0,
|
|
137
|
+
last_dispatch_at INTEGER,
|
|
138
|
+
last_dispatch_outcome TEXT,
|
|
139
|
+
dispatch_scope TEXT, -- persisted channel scope for recovery
|
|
132
140
|
error_message TEXT,
|
|
133
141
|
created_at INTEGER NOT NULL,
|
|
134
142
|
updated_at INTEGER NOT NULL,
|
|
@@ -136,6 +144,8 @@ CREATE TABLE IF NOT EXISTS integration_pending_tasks (
|
|
|
136
144
|
);
|
|
137
145
|
CREATE INDEX IF NOT EXISTS idx_pending_tasks_status_created
|
|
138
146
|
ON integration_pending_tasks(status, created_at);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_pending_tasks_dispatch_scope
|
|
148
|
+
ON integration_pending_tasks(platform, dispatch_scope);
|
|
139
149
|
```
|
|
140
150
|
|
|
141
151
|
The store layer creates this lazily on first use via `ensureTable()` and uses
|
|
@@ -143,10 +153,38 @@ The store layer creates this lazily on first use via `ensureTable()` and uses
|
|
|
143
153
|
|
|
144
154
|
`claimPendingTask` is the critical concurrency primitive: it atomically flips
|
|
145
155
|
`pending` → `processing` and increments `attempts`, returning `null` if another
|
|
146
|
-
worker beat us to it. Both the initial
|
|
156
|
+
worker beat us to it. Both the initial dispatch and every recovery sweep
|
|
147
157
|
funnel through the same processor endpoint, and `claimPendingTask` is what
|
|
148
158
|
prevents the same task from being processed twice.
|
|
149
159
|
|
|
160
|
+
### Netlify durable recovery
|
|
161
|
+
|
|
162
|
+
The portable self-dispatch remains the default. To emit and use Netlify's
|
|
163
|
+
acknowledged background worker plus a one-minute scheduled recovery function,
|
|
164
|
+
set `AGENT_INTEGRATION_DURABLE_DISPATCH=true` at build and runtime. A production
|
|
165
|
+
rollout can be narrowed with the comma-separated
|
|
166
|
+
`AGENT_INTEGRATION_DURABLE_DISPATCH_SCOPES` allowlist. Supported values are
|
|
167
|
+
`<platform>:*`, `<platform>:<external-thread-id>`, and, for adapters that expose
|
|
168
|
+
one, `<platform>:<channel-id>` (for example `slack:C123`).
|
|
169
|
+
Channel-scoped handoffs persist that scope separately from the provider thread
|
|
170
|
+
identity so a later bounded recovery sweep applies the same allowlist decision.
|
|
171
|
+
|
|
172
|
+
Workspace deploys emit this pair only for the Dispatch control-plane app, which
|
|
173
|
+
owns workspace messaging integrations. Standalone templates emit their own
|
|
174
|
+
pair when the same flag is enabled.
|
|
175
|
+
|
|
176
|
+
The background handoff waits only for Netlify's enqueue acknowledgement, not
|
|
177
|
+
for the agent loop. A failed or non-2xx acknowledgement falls back to the
|
|
178
|
+
portable processor. The scheduled function calls the HMAC-authenticated,
|
|
179
|
+
bounded `/retry-stuck-tasks` route; its atomic compare-and-set update prevents
|
|
180
|
+
overlapping sweeps from dispatching the same stale row twice.
|
|
181
|
+
Rows acknowledged by the 15-minute background worker use a 16-minute stale
|
|
182
|
+
cutoff, while portable synchronous dispatches retain the shorter serverless
|
|
183
|
+
cutoff. Apply that lease predicate before `LIMIT` so healthy long runs cannot
|
|
184
|
+
hide recoverable work. The background processor writes its lease marker in the
|
|
185
|
+
same atomic update that claims the row; best-effort dispatch telemetry is not
|
|
186
|
+
trusted for replay safety.
|
|
187
|
+
|
|
150
188
|
## Adding a New Platform Adapter
|
|
151
189
|
|
|
152
190
|
1. **Implement `PlatformAdapter`** in `packages/core/src/integrations/adapters/<platform>.ts`:
|
|
@@ -245,9 +283,10 @@ so a normal long-running reply is safe.
|
|
|
245
283
|
|
|
246
284
|
## Cross-Platform Considerations
|
|
247
285
|
|
|
248
|
-
- **
|
|
249
|
-
|
|
250
|
-
|
|
286
|
+
- **Portable correctness, optional host acceleration.** Every host uses SQL,
|
|
287
|
+
HMAC-authenticated processor routes, and atomic claims. A host-specific
|
|
288
|
+
background function or scheduler may provide a more reliable wake-up, but
|
|
289
|
+
disabling it must preserve the portable path.
|
|
251
290
|
- **No assumed runtime.** The processor endpoint is a normal H3 handler under
|
|
252
291
|
`/_agent-native/`. It runs wherever the rest of the framework runs.
|
|
253
292
|
- **No persistent in-memory state.** The dedup map in the webhook handler is
|
|
@@ -262,11 +301,12 @@ so a normal long-running reply is safe.
|
|
|
262
301
|
|
|
263
302
|
## Why Fire-and-Forget on Serverless Is Unreliable
|
|
264
303
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
complete before the function freezes.
|
|
268
|
-
|
|
269
|
-
|
|
304
|
+
The portable webhook path gives the outbound processor request a short head
|
|
305
|
+
start but does not wait for the response body, so that initial dispatch is
|
|
306
|
+
**not** guaranteed to complete before the function freezes. On a long-lived
|
|
307
|
+
host, the in-process retry loop is a best-effort safety net. On Netlify with
|
|
308
|
+
durable dispatch enabled, the initial handoff is acknowledged and the external
|
|
309
|
+
scheduled function supplies the recovery wake-up for cases where:
|
|
270
310
|
|
|
271
311
|
- The serverless platform froze the handler before the outbound `fetch`
|
|
272
312
|
flushed its bytes.
|
|
@@ -278,8 +318,9 @@ Tasks stuck in `pending` for >90s or `processing` for >5min get re-fired up to
|
|
|
278
318
|
3 times. After 3 attempts they're marked `failed` permanently so we stop
|
|
279
319
|
spamming the processor.
|
|
280
320
|
|
|
281
|
-
**Never assume the initial
|
|
282
|
-
|
|
321
|
+
**Never assume the initial dispatch succeeded.** Rely on the SQL queue, atomic
|
|
322
|
+
claim, and a recovery mechanism whose lifetime is independent of the original
|
|
323
|
+
request. An in-process timer alone is not that mechanism on serverless.
|
|
283
324
|
|
|
284
325
|
## Debugging Checklist
|
|
285
326
|
|
|
@@ -292,7 +333,7 @@ queue + retry job for at-least-once delivery.
|
|
|
292
333
|
4. **Status?** `pending` means the processor never picked it up — check that
|
|
293
334
|
`_process-task` is reachable from the box itself (the self-fetch must work
|
|
294
335
|
over the public URL). `processing` for over 5 minutes means the processor
|
|
295
|
-
died mid-run —
|
|
336
|
+
died mid-run — a live recovery sweep will pick it up.
|
|
296
337
|
5. **Failed?** Check `error_message` and `attempts`. After 3 attempts the row
|
|
297
338
|
is parked at `failed` and won't be retried.
|
|
298
339
|
6. **Reply not delivered?** The processor likely succeeded but
|