@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/durable-background.ts +1 -0
- 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/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/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/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +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 +3 -3
- 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/secrets/routes.d.ts +3 -3
- 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/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/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
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
import { getRequestHeader } from "h3";
|
|
10
10
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
11
11
|
|
|
12
|
+
import {
|
|
13
|
+
AGENT_BACKGROUND_PROCESSOR_FIELD,
|
|
14
|
+
AGENT_BACKGROUND_PROCESSOR_INTEGRATION,
|
|
15
|
+
} from "../agent/durable-background.js";
|
|
12
16
|
import { abortRun } from "../agent/run-manager.js";
|
|
13
17
|
import { getOrgContext, resolveOrgIdForEmail } from "../org/context.js";
|
|
14
18
|
import { loadResourcesForPrompt } from "../server/agent-chat-plugin.js";
|
|
@@ -65,22 +69,27 @@ import {
|
|
|
65
69
|
updateIntegrationInstallation,
|
|
66
70
|
upsertIntegrationInstallation,
|
|
67
71
|
} from "./installations-store.js";
|
|
72
|
+
import {
|
|
73
|
+
dispatchPendingIntegrationTask,
|
|
74
|
+
INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT,
|
|
75
|
+
integrationDispatchScopeValue,
|
|
76
|
+
isIntegrationDurableDispatchConfigured,
|
|
77
|
+
} from "./integration-durable-dispatch.js";
|
|
68
78
|
import {
|
|
69
79
|
forgetIntegrationMemory,
|
|
70
80
|
integrationMemoryActions,
|
|
71
81
|
listIntegrationMemory,
|
|
72
82
|
rememberForIntegrationScope,
|
|
73
83
|
} from "./integration-memory.js";
|
|
84
|
+
import { extractBearerToken, verifyInternalToken } from "./internal-token.js";
|
|
74
85
|
import {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
} from "./internal-token.js";
|
|
79
|
-
import { startPendingTasksRetryJob } from "./pending-tasks-retry-job.js";
|
|
86
|
+
retryStuckPendingTasks,
|
|
87
|
+
startPendingTasksRetryJob,
|
|
88
|
+
} from "./pending-tasks-retry-job.js";
|
|
80
89
|
import {
|
|
81
90
|
claimPendingTask,
|
|
82
91
|
failTaskDeliveryTransition,
|
|
83
|
-
|
|
92
|
+
getNextPendingTaskForThread,
|
|
84
93
|
insertPendingTask,
|
|
85
94
|
isDuplicateEventError,
|
|
86
95
|
MAX_PENDING_TASK_ATTEMPTS,
|
|
@@ -725,40 +734,29 @@ export function createIntegrationsPlugin(
|
|
|
725
734
|
externalEventKey: opts?.dedupeKey
|
|
726
735
|
? systemNoticeEventKey(opts.dedupeKey, dedupeTtlMs)
|
|
727
736
|
: undefined,
|
|
737
|
+
dispatchScope: integrationDispatchScopeValue({
|
|
738
|
+
platform: incoming.platform,
|
|
739
|
+
externalThreadId: noticeThreadId,
|
|
740
|
+
platformContext: incoming.platformContext,
|
|
741
|
+
}),
|
|
728
742
|
});
|
|
729
743
|
} catch (err) {
|
|
730
744
|
if (isDuplicateEventError(err)) return;
|
|
731
745
|
throw err;
|
|
732
746
|
}
|
|
733
747
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
} catch (err) {
|
|
741
|
-
if (process.env.NODE_ENV === "production") throw err;
|
|
742
|
-
}
|
|
743
|
-
void fetch(`${getBaseUrl(event)}${P}/process-task`, {
|
|
744
|
-
method: "POST",
|
|
745
|
-
headers: {
|
|
746
|
-
"Content-Type": "application/json",
|
|
747
|
-
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
748
|
+
await dispatchPendingIntegrationTask({
|
|
749
|
+
taskId,
|
|
750
|
+
task: {
|
|
751
|
+
platform: incoming.platform,
|
|
752
|
+
externalThreadId: noticeThreadId,
|
|
753
|
+
platformContext: incoming.platformContext,
|
|
748
754
|
},
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
console.warn("[integrations] System notice dispatch failed:", err);
|
|
755
|
+
event,
|
|
756
|
+
baseUrl: getBaseUrl(event),
|
|
752
757
|
});
|
|
753
758
|
}
|
|
754
759
|
|
|
755
|
-
async function requireSession(event: any): Promise<boolean> {
|
|
756
|
-
const session = await getSession(event).catch(() => null);
|
|
757
|
-
if (session?.email) return true;
|
|
758
|
-
setResponseStatus(event, 401);
|
|
759
|
-
return false;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
760
|
async function requireSessionContext(
|
|
763
761
|
event: any,
|
|
764
762
|
): Promise<{ ownerEmail: string; orgId: string | null } | null> {
|
|
@@ -903,9 +901,10 @@ export function createIntegrationsPlugin(
|
|
|
903
901
|
setResponseStatus(event, 405);
|
|
904
902
|
return { error: "Method not allowed" };
|
|
905
903
|
}
|
|
906
|
-
|
|
904
|
+
const scope = await requireSessionContext(event);
|
|
905
|
+
if (!scope) return { error: "unauthorized" };
|
|
907
906
|
try {
|
|
908
|
-
return await getTaskQueueStats();
|
|
907
|
+
return await getTaskQueueStats(scope);
|
|
909
908
|
} catch (err: any) {
|
|
910
909
|
setResponseStatus(event, 500);
|
|
911
910
|
return { error: err?.message ?? String(err) };
|
|
@@ -1595,10 +1594,53 @@ export function createIntegrationsPlugin(
|
|
|
1595
1594
|
}),
|
|
1596
1595
|
);
|
|
1597
1596
|
|
|
1597
|
+
// ─── Durable pending-task recovery sweep ─────────────────────
|
|
1598
|
+
h3.use(
|
|
1599
|
+
`${P}/retry-stuck-tasks`,
|
|
1600
|
+
defineEventHandler(async (event) => {
|
|
1601
|
+
if (getMethod(event) !== "POST") {
|
|
1602
|
+
setResponseStatus(event, 405);
|
|
1603
|
+
return { error: "Method not allowed" };
|
|
1604
|
+
}
|
|
1605
|
+
const body = (await readBody(event)) as {
|
|
1606
|
+
taskId?: string;
|
|
1607
|
+
[AGENT_BACKGROUND_PROCESSOR_FIELD]?: string;
|
|
1608
|
+
};
|
|
1609
|
+
if (body?.taskId !== INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT) {
|
|
1610
|
+
setResponseStatus(event, 400);
|
|
1611
|
+
return { error: "invalid sweep subject" };
|
|
1612
|
+
}
|
|
1613
|
+
if (!process.env.A2A_SECRET) {
|
|
1614
|
+
setResponseStatus(event, 503);
|
|
1615
|
+
return { error: "durable integration recovery is not configured" };
|
|
1616
|
+
}
|
|
1617
|
+
const token = extractBearerToken(
|
|
1618
|
+
getRequestHeader(event, "authorization"),
|
|
1619
|
+
);
|
|
1620
|
+
if (
|
|
1621
|
+
!token ||
|
|
1622
|
+
!verifyInternalToken(INTEGRATION_RETRY_SWEEP_TOKEN_SUBJECT, token)
|
|
1623
|
+
) {
|
|
1624
|
+
setResponseStatus(event, 401);
|
|
1625
|
+
return { error: "Invalid or expired internal token" };
|
|
1626
|
+
}
|
|
1627
|
+
if (!isIntegrationDurableDispatchConfigured()) {
|
|
1628
|
+
return { ok: true, disabled: true };
|
|
1629
|
+
}
|
|
1630
|
+
const result = await retryStuckPendingTasks({
|
|
1631
|
+
webhookBaseUrl: getBaseUrl(event),
|
|
1632
|
+
limit: 20,
|
|
1633
|
+
durableOnly: true,
|
|
1634
|
+
});
|
|
1635
|
+
return { ok: true, ...result };
|
|
1636
|
+
}),
|
|
1637
|
+
);
|
|
1638
|
+
|
|
1598
1639
|
// ─── Process pending task (cross-platform task queue) ────────
|
|
1599
1640
|
// POST /_agent-native/integrations/process-task
|
|
1600
|
-
// Internal endpoint invoked
|
|
1601
|
-
//
|
|
1641
|
+
// Internal endpoint invoked from the public webhook handler through either
|
|
1642
|
+
// the portable self-dispatch path or an acknowledged background handoff.
|
|
1643
|
+
// Auth: HMAC bearer signed with A2A_SECRET.
|
|
1602
1644
|
// Each invocation runs the agent loop in a fresh function execution.
|
|
1603
1645
|
h3.use(
|
|
1604
1646
|
`${P}/process-task`,
|
|
@@ -1608,7 +1650,10 @@ export function createIntegrationsPlugin(
|
|
|
1608
1650
|
return { error: "Method not allowed" };
|
|
1609
1651
|
}
|
|
1610
1652
|
|
|
1611
|
-
const body = (await readBody(event)) as {
|
|
1653
|
+
const body = (await readBody(event)) as {
|
|
1654
|
+
taskId?: string;
|
|
1655
|
+
[AGENT_BACKGROUND_PROCESSOR_FIELD]?: string;
|
|
1656
|
+
};
|
|
1612
1657
|
const taskId = body?.taskId;
|
|
1613
1658
|
if (!taskId) {
|
|
1614
1659
|
setResponseStatus(event, 400);
|
|
@@ -1645,7 +1690,12 @@ export function createIntegrationsPlugin(
|
|
|
1645
1690
|
}
|
|
1646
1691
|
|
|
1647
1692
|
// Atomic claim: only one invocation gets to process this task
|
|
1648
|
-
const
|
|
1693
|
+
const dispatchOutcome =
|
|
1694
|
+
body[AGENT_BACKGROUND_PROCESSOR_FIELD] ===
|
|
1695
|
+
AGENT_BACKGROUND_PROCESSOR_INTEGRATION
|
|
1696
|
+
? "background-acknowledged"
|
|
1697
|
+
: "portable-unconfirmed";
|
|
1698
|
+
const task = await claimPendingTask(taskId, { dispatchOutcome });
|
|
1649
1699
|
if (!task) {
|
|
1650
1700
|
setResponseStatus(event, 200);
|
|
1651
1701
|
return { ok: true, skipped: "already-claimed-or-missing" };
|
|
@@ -1779,24 +1829,22 @@ export function createIntegrationsPlugin(
|
|
|
1779
1829
|
return { ok: true, taskId, retrying: "response-delivery" };
|
|
1780
1830
|
}
|
|
1781
1831
|
await markTaskCompleted(taskId);
|
|
1782
|
-
const
|
|
1832
|
+
const nextTask = await getNextPendingTaskForThread(
|
|
1783
1833
|
task.platform,
|
|
1784
1834
|
task.externalThreadId,
|
|
1785
1835
|
);
|
|
1786
|
-
if (
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1836
|
+
if (nextTask) {
|
|
1837
|
+
await dispatchPendingIntegrationTask({
|
|
1838
|
+
taskId: nextTask.id,
|
|
1839
|
+
task: {
|
|
1840
|
+
platform: task.platform,
|
|
1841
|
+
externalThreadId: task.externalThreadId,
|
|
1842
|
+
platformContext: nextTask.dispatchScope
|
|
1843
|
+
? { channelId: nextTask.dispatchScope }
|
|
1844
|
+
: undefined,
|
|
1793
1845
|
},
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
console.error(
|
|
1797
|
-
"[integrations] Failed to dispatch queued thread turn:",
|
|
1798
|
-
err,
|
|
1799
|
-
);
|
|
1846
|
+
event,
|
|
1847
|
+
baseUrl: getBaseUrl(event),
|
|
1800
1848
|
});
|
|
1801
1849
|
}
|
|
1802
1850
|
await processDueA2AContinuations({
|
|
@@ -2018,18 +2066,21 @@ export function createIntegrationsPlugin(
|
|
|
2018
2066
|
ownerEmail: control.ownerEmail,
|
|
2019
2067
|
orgId: control.orgId,
|
|
2020
2068
|
externalEventKey: `control:${control.id}`,
|
|
2069
|
+
dispatchScope: integrationDispatchScopeValue({
|
|
2070
|
+
platform: incoming.platform,
|
|
2071
|
+
externalThreadId: incoming.externalThreadId,
|
|
2072
|
+
platformContext: incoming.platformContext,
|
|
2073
|
+
}),
|
|
2021
2074
|
});
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
2075
|
+
await dispatchPendingIntegrationTask({
|
|
2076
|
+
taskId,
|
|
2077
|
+
task: {
|
|
2078
|
+
platform: incoming.platform,
|
|
2079
|
+
externalThreadId: incoming.externalThreadId,
|
|
2080
|
+
platformContext: incoming.platformContext,
|
|
2029
2081
|
},
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
console.error("[slack] Approval dispatch failed:", err);
|
|
2082
|
+
event,
|
|
2083
|
+
baseUrl: getBaseUrl(event),
|
|
2033
2084
|
});
|
|
2034
2085
|
} catch (err) {
|
|
2035
2086
|
console.error("[slack] Interaction handling failed:", err);
|
|
@@ -2480,6 +2531,8 @@ export function createIntegrationsPlugin(
|
|
|
2480
2531
|
if (parts[0] === "remote") return;
|
|
2481
2532
|
// Already handled by the dedicated /process-task route above
|
|
2482
2533
|
if (parts[0] === "process-task") return;
|
|
2534
|
+
// Already handled by the signed durable recovery route above
|
|
2535
|
+
if (parts[0] === "retry-stuck-tasks") return;
|
|
2483
2536
|
// Already handled by the dedicated /process-a2a-continuation route above
|
|
2484
2537
|
if (parts[0] === "process-a2a-continuation") return;
|
|
2485
2538
|
// These are framework-owned control-plane routes, not integration
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Lives in its own file so it stays out of `pending-tasks-store.ts`, which is
|
|
5
5
|
* actively being edited by the agent that owns the queue itself. These
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Queue reads never expose payloads or user text. The helper first runs the
|
|
7
|
+
* additive schema guard so older deployments gain the dispatch diagnostic
|
|
8
|
+
* columns before the SELECTs execute.
|
|
9
9
|
*/
|
|
10
10
|
import { getDbExec } from "../db/client.js";
|
|
11
|
+
import { ensurePendingTasksTable } from "./pending-tasks-store.js";
|
|
11
12
|
|
|
12
13
|
export interface RecentFailure {
|
|
13
14
|
id: string;
|
|
@@ -23,6 +24,20 @@ export interface TaskQueueStats {
|
|
|
23
24
|
failed_last_hour: number;
|
|
24
25
|
oldest_pending_age_seconds: number;
|
|
25
26
|
recent_failures: RecentFailure[];
|
|
27
|
+
recent_tasks: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
platform: string;
|
|
30
|
+
status: string;
|
|
31
|
+
attempts: number;
|
|
32
|
+
dispatch_attempts: number;
|
|
33
|
+
last_dispatch_outcome: string | null;
|
|
34
|
+
age_seconds: number;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TaskQueueStatsScope {
|
|
39
|
+
ownerEmail: string;
|
|
40
|
+
orgId: string | null;
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
const ZERO_STATS: TaskQueueStats = {
|
|
@@ -32,6 +47,7 @@ const ZERO_STATS: TaskQueueStats = {
|
|
|
32
47
|
failed_last_hour: 0,
|
|
33
48
|
oldest_pending_age_seconds: 0,
|
|
34
49
|
recent_failures: [],
|
|
50
|
+
recent_tasks: [],
|
|
35
51
|
};
|
|
36
52
|
|
|
37
53
|
function isMissingTableError(err: unknown): boolean {
|
|
@@ -44,21 +60,27 @@ function isMissingTableError(err: unknown): boolean {
|
|
|
44
60
|
/**
|
|
45
61
|
* Get a snapshot of the integration task queue health.
|
|
46
62
|
*
|
|
47
|
-
*
|
|
48
|
-
* pending-tasks store has initialised the schema.
|
|
63
|
+
* Safe to call before the pending-tasks store has initialized the schema.
|
|
49
64
|
*/
|
|
50
|
-
export async function getTaskQueueStats(
|
|
65
|
+
export async function getTaskQueueStats(
|
|
66
|
+
scope: TaskQueueStatsScope,
|
|
67
|
+
): Promise<TaskQueueStats> {
|
|
68
|
+
await ensurePendingTasksTable();
|
|
51
69
|
const client = getDbExec();
|
|
52
70
|
const now = Date.now();
|
|
53
71
|
const oneHourAgo = now - 60 * 60 * 1000;
|
|
72
|
+
const scopeSql = `owner_email = ?
|
|
73
|
+
AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)`;
|
|
74
|
+
const scopeArgs = [scope.ownerEmail, scope.orgId, scope.orgId];
|
|
54
75
|
|
|
55
76
|
try {
|
|
56
77
|
// Status counts (pending, processing) — only need the live ones.
|
|
57
78
|
const liveCounts = await client.execute({
|
|
58
79
|
sql: `SELECT status, COUNT(*) AS c FROM integration_pending_tasks
|
|
59
|
-
WHERE
|
|
80
|
+
WHERE ${scopeSql}
|
|
81
|
+
AND status IN ('pending', 'processing')
|
|
60
82
|
GROUP BY status`,
|
|
61
|
-
args:
|
|
83
|
+
args: scopeArgs,
|
|
62
84
|
});
|
|
63
85
|
|
|
64
86
|
let pending = 0;
|
|
@@ -75,9 +97,10 @@ export async function getTaskQueueStats(): Promise<TaskQueueStats> {
|
|
|
75
97
|
// miss tasks queued >1h ago that just finished now.
|
|
76
98
|
const lastHourCounts = await client.execute({
|
|
77
99
|
sql: `SELECT status, COUNT(*) AS c FROM integration_pending_tasks
|
|
78
|
-
WHERE
|
|
100
|
+
WHERE ${scopeSql}
|
|
101
|
+
AND status IN ('completed', 'failed') AND updated_at >= ?
|
|
79
102
|
GROUP BY status`,
|
|
80
|
-
args: [oneHourAgo],
|
|
103
|
+
args: [...scopeArgs, oneHourAgo],
|
|
81
104
|
});
|
|
82
105
|
|
|
83
106
|
let completedLastHour = 0;
|
|
@@ -94,10 +117,11 @@ export async function getTaskQueueStats(): Promise<TaskQueueStats> {
|
|
|
94
117
|
if (pending > 0) {
|
|
95
118
|
const oldest = await client.execute({
|
|
96
119
|
sql: `SELECT created_at FROM integration_pending_tasks
|
|
97
|
-
WHERE
|
|
120
|
+
WHERE ${scopeSql}
|
|
121
|
+
AND status = 'pending'
|
|
98
122
|
ORDER BY created_at ASC
|
|
99
123
|
LIMIT 1`,
|
|
100
|
-
args:
|
|
124
|
+
args: scopeArgs,
|
|
101
125
|
});
|
|
102
126
|
const oldestRow = oldest.rows[0] as Record<string, unknown> | undefined;
|
|
103
127
|
if (oldestRow) {
|
|
@@ -113,10 +137,11 @@ export async function getTaskQueueStats(): Promise<TaskQueueStats> {
|
|
|
113
137
|
// blowing up the response payload.
|
|
114
138
|
const failures = await client.execute({
|
|
115
139
|
sql: `SELECT id, platform, error_message, attempts FROM integration_pending_tasks
|
|
116
|
-
WHERE
|
|
140
|
+
WHERE ${scopeSql}
|
|
141
|
+
AND status = 'failed' AND updated_at >= ?
|
|
117
142
|
ORDER BY updated_at DESC
|
|
118
143
|
LIMIT 5`,
|
|
119
|
-
args: [oneHourAgo],
|
|
144
|
+
args: [...scopeArgs, oneHourAgo],
|
|
120
145
|
});
|
|
121
146
|
const recentFailures: RecentFailure[] = (
|
|
122
147
|
failures.rows as Array<Record<string, unknown>>
|
|
@@ -127,6 +152,33 @@ export async function getTaskQueueStats(): Promise<TaskQueueStats> {
|
|
|
127
152
|
attempts: Number(row.attempts ?? 0),
|
|
128
153
|
}));
|
|
129
154
|
|
|
155
|
+
const recent = await client.execute({
|
|
156
|
+
sql: `SELECT id, platform, status, attempts, dispatch_attempts,
|
|
157
|
+
last_dispatch_outcome, created_at
|
|
158
|
+
FROM integration_pending_tasks
|
|
159
|
+
WHERE ${scopeSql}
|
|
160
|
+
ORDER BY created_at DESC
|
|
161
|
+
LIMIT 10`,
|
|
162
|
+
args: scopeArgs,
|
|
163
|
+
});
|
|
164
|
+
const recentTasks = (recent.rows as Array<Record<string, unknown>>).map(
|
|
165
|
+
(row) => ({
|
|
166
|
+
id: String(row.id ?? ""),
|
|
167
|
+
platform: String(row.platform ?? ""),
|
|
168
|
+
status: String(row.status ?? ""),
|
|
169
|
+
attempts: Number(row.attempts ?? 0),
|
|
170
|
+
dispatch_attempts: Number(row.dispatch_attempts ?? 0),
|
|
171
|
+
last_dispatch_outcome:
|
|
172
|
+
row.last_dispatch_outcome == null
|
|
173
|
+
? null
|
|
174
|
+
: String(row.last_dispatch_outcome),
|
|
175
|
+
age_seconds: Math.max(
|
|
176
|
+
0,
|
|
177
|
+
Math.floor((now - Number(row.created_at ?? now)) / 1000),
|
|
178
|
+
),
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
|
|
130
182
|
return {
|
|
131
183
|
pending,
|
|
132
184
|
processing,
|
|
@@ -134,10 +186,11 @@ export async function getTaskQueueStats(): Promise<TaskQueueStats> {
|
|
|
134
186
|
failed_last_hour: failedLastHour,
|
|
135
187
|
oldest_pending_age_seconds: oldestPendingAgeSeconds,
|
|
136
188
|
recent_failures: recentFailures,
|
|
189
|
+
recent_tasks: recentTasks,
|
|
137
190
|
};
|
|
138
191
|
} catch (err) {
|
|
139
192
|
if (isMissingTableError(err)) {
|
|
140
|
-
return { ...ZERO_STATS, recent_failures: [] };
|
|
193
|
+
return { ...ZERO_STATS, recent_failures: [], recent_tasks: [] };
|
|
141
194
|
}
|
|
142
195
|
throw err;
|
|
143
196
|
}
|
|
@@ -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
|
/**
|
|
@@ -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.
|