@agent-native/core 0.107.1 → 0.107.2
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/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/artifact-response.ts +32 -10
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +107 -3
- package/corpus/core/src/integrations/pending-tasks-store.ts +98 -1
- package/corpus/core/src/integrations/plugin.ts +129 -4
- package/corpus/core/src/integrations/webhook-handler.ts +339 -36
- package/corpus/core/src/server/agent-chat-plugin.ts +93 -8
- package/dist/a2a/artifact-response.d.ts +5 -1
- package/dist/a2a/artifact-response.d.ts.map +1 -1
- package/dist/a2a/artifact-response.js +18 -12
- package/dist/a2a/artifact-response.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/integrations/a2a-continuation-processor.js +62 -5
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/integrations/pending-tasks-store.d.ts +3 -0
- package/dist/integrations/pending-tasks-store.d.ts.map +1 -1
- package/dist/integrations/pending-tasks-store.js +75 -1
- package/dist/integrations/pending-tasks-store.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +81 -5
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/webhook-handler.d.ts +23 -2
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +257 -35
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-chat-plugin.d.ts +6 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +71 -6
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
package/corpus/core/CHANGELOG.md
CHANGED
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.107.
|
|
3
|
+
"version": "0.107.2",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -11,6 +11,11 @@ export interface A2AArtifactResponseOptions {
|
|
|
11
11
|
baseUrl?: string;
|
|
12
12
|
includeReferencedArtifacts?: boolean;
|
|
13
13
|
includePersistedArtifactMarker?: boolean;
|
|
14
|
+
persistedArtifactSecret?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface A2AArtifactIdentityOptions {
|
|
18
|
+
persistedArtifactSecrets?: readonly string[];
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
export interface A2AArtifactIdentity {
|
|
@@ -74,21 +79,26 @@ const ARTIFACT_RESOURCE_TYPES = new Set<A2AArtifactIdentity["resourceType"]>([
|
|
|
74
79
|
|
|
75
80
|
function persistedArtifactIdentitiesFromMarker(
|
|
76
81
|
result: string,
|
|
82
|
+
secrets: readonly string[] = process.env.A2A_SECRET
|
|
83
|
+
? [process.env.A2A_SECRET]
|
|
84
|
+
: [],
|
|
77
85
|
): A2AArtifactIdentity[] {
|
|
78
|
-
|
|
79
|
-
if (!secret) return [];
|
|
86
|
+
if (secrets.length === 0) return [];
|
|
80
87
|
const match = result.match(
|
|
81
88
|
/<!--\s*agent-native:persisted-artifacts=([A-Za-z0-9_-]+)\.([a-f0-9]{64})\s*-->/,
|
|
82
89
|
);
|
|
83
90
|
if (!match) return [];
|
|
84
91
|
try {
|
|
85
92
|
const payload = match[1];
|
|
86
|
-
const expected = createHmac("sha256", secret).update(payload).digest();
|
|
87
93
|
const supplied = Buffer.from(match[2], "hex");
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
const verified = secrets.some((secret) => {
|
|
95
|
+
const expected = createHmac("sha256", secret).update(payload).digest();
|
|
96
|
+
return (
|
|
97
|
+
supplied.length === expected.length &&
|
|
98
|
+
timingSafeEqual(supplied, expected)
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
if (!verified) {
|
|
92
102
|
return [];
|
|
93
103
|
}
|
|
94
104
|
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString());
|
|
@@ -114,9 +124,15 @@ function persistedArtifactIdentitiesFromMarker(
|
|
|
114
124
|
function withPersistedArtifactMarker(
|
|
115
125
|
text: string,
|
|
116
126
|
toolResults: A2AToolResultSummary[],
|
|
127
|
+
secret = process.env.A2A_SECRET,
|
|
117
128
|
): string {
|
|
118
|
-
const
|
|
119
|
-
|
|
129
|
+
const verificationSecrets = [secret, process.env.A2A_SECRET].filter(
|
|
130
|
+
(value, index, values): value is string =>
|
|
131
|
+
!!value && values.indexOf(value) === index,
|
|
132
|
+
);
|
|
133
|
+
const identities = extractA2AArtifactIdentities(toolResults, {
|
|
134
|
+
persistedArtifactSecrets: verificationSecrets,
|
|
135
|
+
}).slice(0, 12);
|
|
120
136
|
if (identities.length === 0 || !secret) return text;
|
|
121
137
|
const payload = Buffer.from(JSON.stringify(identities)).toString("base64url");
|
|
122
138
|
const signature = createHmac("sha256", secret).update(payload).digest("hex");
|
|
@@ -881,6 +897,7 @@ function collectArtifacts(results: A2AToolResultSummary[]): {
|
|
|
881
897
|
*/
|
|
882
898
|
export function extractA2AArtifactIdentities(
|
|
883
899
|
results: A2AToolResultSummary[],
|
|
900
|
+
options: A2AArtifactIdentityOptions = {},
|
|
884
901
|
): A2AArtifactIdentity[] {
|
|
885
902
|
const identities = new Map<string, A2AArtifactIdentity>();
|
|
886
903
|
|
|
@@ -894,6 +911,7 @@ export function extractA2AArtifactIdentities(
|
|
|
894
911
|
if (result.tool === "call-agent") {
|
|
895
912
|
for (const identity of persistedArtifactIdentitiesFromMarker(
|
|
896
913
|
result.result,
|
|
914
|
+
options.persistedArtifactSecrets,
|
|
897
915
|
)) {
|
|
898
916
|
remember({ ...identity, sourceAction: "call-agent" });
|
|
899
917
|
}
|
|
@@ -1387,7 +1405,11 @@ export function appendA2AArtifactLinks(
|
|
|
1387
1405
|
options.includeReferencedArtifacts ?? false;
|
|
1388
1406
|
const finalize = (value: string) =>
|
|
1389
1407
|
options.includePersistedArtifactMarker
|
|
1390
|
-
? withPersistedArtifactMarker(
|
|
1408
|
+
? withPersistedArtifactMarker(
|
|
1409
|
+
value,
|
|
1410
|
+
toolResults,
|
|
1411
|
+
options.persistedArtifactSecret ?? process.env.A2A_SECRET,
|
|
1412
|
+
)
|
|
1391
1413
|
: value;
|
|
1392
1414
|
const {
|
|
1393
1415
|
documents,
|
|
@@ -171,11 +171,22 @@ async function processClaimedContinuation(
|
|
|
171
171
|
...(auth.apiKeyFallbacks ? { fallbackApiKeys: auth.apiKeyFallbacks } : {}),
|
|
172
172
|
});
|
|
173
173
|
const deadline = Date.now() + PROCESSOR_WAIT_MS;
|
|
174
|
+
const recoverableArtifactSecrets =
|
|
175
|
+
await resolveContinuationArtifactSecrets(continuation);
|
|
174
176
|
let task: Task | null = null;
|
|
177
|
+
let latestRecoverableArtifactText: string | null = null;
|
|
175
178
|
|
|
176
179
|
try {
|
|
177
180
|
while (Date.now() < deadline) {
|
|
178
181
|
task = await client.getTask(continuation.a2aTaskId);
|
|
182
|
+
const recoverableArtifactText = extractVerifiedRecoverableArtifactText(
|
|
183
|
+
task,
|
|
184
|
+
continuation.agentUrl,
|
|
185
|
+
recoverableArtifactSecrets,
|
|
186
|
+
);
|
|
187
|
+
if (recoverableArtifactText) {
|
|
188
|
+
latestRecoverableArtifactText = recoverableArtifactText;
|
|
189
|
+
}
|
|
179
190
|
if (TERMINAL_STATES.has(task.status.state)) break;
|
|
180
191
|
await reportA2AContinuationProgress(continuation, progress, task);
|
|
181
192
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
@@ -183,6 +194,17 @@ async function processClaimedContinuation(
|
|
|
183
194
|
} catch (err) {
|
|
184
195
|
if (isTransientA2APollError(err)) {
|
|
185
196
|
if (shouldStopPollingRemoteTask(continuation)) {
|
|
197
|
+
if (latestRecoverableArtifactText) {
|
|
198
|
+
await deliverAndCompleteA2AContinuation(
|
|
199
|
+
continuation,
|
|
200
|
+
adapter,
|
|
201
|
+
formatRecoverableArtifactFallbackText(
|
|
202
|
+
latestRecoverableArtifactText,
|
|
203
|
+
),
|
|
204
|
+
progress,
|
|
205
|
+
);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
186
208
|
await notifyAndFailA2AContinuation(
|
|
187
209
|
continuation,
|
|
188
210
|
adapter,
|
|
@@ -209,6 +231,15 @@ async function processClaimedContinuation(
|
|
|
209
231
|
|
|
210
232
|
if (!task || !TERMINAL_STATES.has(task.status.state)) {
|
|
211
233
|
if (shouldStopPollingRemoteTask(continuation)) {
|
|
234
|
+
if (latestRecoverableArtifactText) {
|
|
235
|
+
await deliverAndCompleteA2AContinuation(
|
|
236
|
+
continuation,
|
|
237
|
+
adapter,
|
|
238
|
+
formatRecoverableArtifactFallbackText(latestRecoverableArtifactText),
|
|
239
|
+
progress,
|
|
240
|
+
);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
212
243
|
await notifyAndFailA2AContinuation(
|
|
213
244
|
continuation,
|
|
214
245
|
adapter,
|
|
@@ -222,6 +253,15 @@ async function processClaimedContinuation(
|
|
|
222
253
|
}
|
|
223
254
|
|
|
224
255
|
if (task.status.state !== "completed") {
|
|
256
|
+
if (latestRecoverableArtifactText) {
|
|
257
|
+
await deliverAndCompleteA2AContinuation(
|
|
258
|
+
continuation,
|
|
259
|
+
adapter,
|
|
260
|
+
formatRecoverableArtifactFallbackText(latestRecoverableArtifactText),
|
|
261
|
+
progress,
|
|
262
|
+
);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
225
265
|
const reason =
|
|
226
266
|
extractTaskText(task) ||
|
|
227
267
|
`Remote A2A task ${continuation.a2aTaskId} ended with state ${task.status.state}`;
|
|
@@ -234,6 +274,15 @@ async function processClaimedContinuation(
|
|
|
234
274
|
continuation.agentUrl,
|
|
235
275
|
);
|
|
236
276
|
if (!text.trim()) {
|
|
277
|
+
if (latestRecoverableArtifactText) {
|
|
278
|
+
await deliverAndCompleteA2AContinuation(
|
|
279
|
+
continuation,
|
|
280
|
+
adapter,
|
|
281
|
+
formatRecoverableArtifactFallbackText(latestRecoverableArtifactText),
|
|
282
|
+
progress,
|
|
283
|
+
);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
237
286
|
await notifyAndFailA2AContinuation(
|
|
238
287
|
continuation,
|
|
239
288
|
adapter,
|
|
@@ -382,6 +431,8 @@ async function deliverAndCompleteA2AContinuation(
|
|
|
382
431
|
`${deliveryContinuation.platform} response delivery timed out`,
|
|
383
432
|
);
|
|
384
433
|
let persistenceError: unknown;
|
|
434
|
+
const artifactSecrets =
|
|
435
|
+
await resolveContinuationArtifactSecrets(deliveryContinuation);
|
|
385
436
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
386
437
|
try {
|
|
387
438
|
await persistA2AContinuationDelivery(
|
|
@@ -389,6 +440,7 @@ async function deliverAndCompleteA2AContinuation(
|
|
|
389
440
|
outgoing,
|
|
390
441
|
deliveryReceipt,
|
|
391
442
|
text,
|
|
443
|
+
artifactSecrets,
|
|
392
444
|
);
|
|
393
445
|
persistenceError = undefined;
|
|
394
446
|
break;
|
|
@@ -463,6 +515,7 @@ async function persistA2AContinuationDelivery(
|
|
|
463
515
|
outgoing: OutgoingMessage,
|
|
464
516
|
receipt: PlatformDeliveryReceipt,
|
|
465
517
|
artifactText: string,
|
|
518
|
+
artifactSecrets: readonly string[],
|
|
466
519
|
): Promise<void> {
|
|
467
520
|
const mapping = await getThreadMapping(
|
|
468
521
|
continuation.platform,
|
|
@@ -480,9 +533,12 @@ async function persistA2AContinuationDelivery(
|
|
|
480
533
|
}
|
|
481
534
|
if (!Array.isArray(repo.messages)) repo.messages = [];
|
|
482
535
|
|
|
483
|
-
const artifacts = extractA2AArtifactIdentities(
|
|
484
|
-
{ tool: "call-agent", result: artifactText },
|
|
485
|
-
|
|
536
|
+
const artifacts = extractA2AArtifactIdentities(
|
|
537
|
+
[{ tool: "call-agent", result: artifactText }],
|
|
538
|
+
{
|
|
539
|
+
persistedArtifactSecrets: artifactSecrets,
|
|
540
|
+
},
|
|
541
|
+
);
|
|
486
542
|
const metadata: Record<string, unknown> = {
|
|
487
543
|
integrationDeliveryAttempted: true,
|
|
488
544
|
integrationDelivery: {
|
|
@@ -726,6 +782,24 @@ async function signFreshContinuationTokens(
|
|
|
726
782
|
return tokens;
|
|
727
783
|
}
|
|
728
784
|
|
|
785
|
+
async function resolveContinuationArtifactSecrets(
|
|
786
|
+
continuation: A2AContinuation,
|
|
787
|
+
): Promise<string[]> {
|
|
788
|
+
const secrets: string[] = [];
|
|
789
|
+
const add = (secret: string | null | undefined) => {
|
|
790
|
+
const value = secret?.trim();
|
|
791
|
+
if (value && !secrets.includes(value)) secrets.push(value);
|
|
792
|
+
};
|
|
793
|
+
add(process.env.A2A_SECRET);
|
|
794
|
+
if (continuation.orgId) {
|
|
795
|
+
try {
|
|
796
|
+
const { getOrgA2ASecret } = await import("../org/context.js");
|
|
797
|
+
add(await getOrgA2ASecret(continuation.orgId));
|
|
798
|
+
} catch {}
|
|
799
|
+
}
|
|
800
|
+
return secrets;
|
|
801
|
+
}
|
|
802
|
+
|
|
729
803
|
function isLikelyJwt(token: string): boolean {
|
|
730
804
|
return token.split(".").length === 3;
|
|
731
805
|
}
|
|
@@ -740,6 +814,36 @@ function extractTaskText(task: Task): string {
|
|
|
740
814
|
.join("\n");
|
|
741
815
|
}
|
|
742
816
|
|
|
817
|
+
function extractVerifiedRecoverableArtifactText(
|
|
818
|
+
task: Task,
|
|
819
|
+
agentUrl: string,
|
|
820
|
+
artifactSecrets: readonly string[],
|
|
821
|
+
): string | null {
|
|
822
|
+
if (task.status.message?.metadata?.agentNativeRecoverableArtifacts !== true) {
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const text = formatContinuationArtifactText(extractTaskText(task), agentUrl);
|
|
827
|
+
if (!text.trim()) return null;
|
|
828
|
+
|
|
829
|
+
// Require the signed identity ledger so arbitrary peer progress prose cannot
|
|
830
|
+
// prematurely complete the continuation.
|
|
831
|
+
const artifacts = extractA2AArtifactIdentities(
|
|
832
|
+
[{ tool: "call-agent", result: text }],
|
|
833
|
+
{
|
|
834
|
+
persistedArtifactSecrets: artifactSecrets,
|
|
835
|
+
},
|
|
836
|
+
);
|
|
837
|
+
return artifacts.length > 0 ? text : null;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function formatRecoverableArtifactFallbackText(text: string): string {
|
|
841
|
+
return text.replace(
|
|
842
|
+
"The agent is still working on the full response, but these verified artifacts already exist:",
|
|
843
|
+
"The downstream agent did not finish its full response, but these verified artifacts already exist:",
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
|
|
743
847
|
function formatContinuationArtifactText(
|
|
744
848
|
text: string,
|
|
745
849
|
agentUrl: string,
|
|
@@ -272,6 +272,19 @@ export async function claimPendingTask(
|
|
|
272
272
|
AND active.status = 'processing'
|
|
273
273
|
AND active.id <> integration_pending_tasks.id
|
|
274
274
|
)
|
|
275
|
+
AND NOT EXISTS (
|
|
276
|
+
SELECT 1 FROM integration_pending_tasks earlier
|
|
277
|
+
WHERE earlier.platform = integration_pending_tasks.platform
|
|
278
|
+
AND earlier.external_thread_id = integration_pending_tasks.external_thread_id
|
|
279
|
+
AND earlier.status = 'pending'
|
|
280
|
+
AND (
|
|
281
|
+
earlier.created_at < integration_pending_tasks.created_at
|
|
282
|
+
OR (
|
|
283
|
+
earlier.created_at = integration_pending_tasks.created_at
|
|
284
|
+
AND earlier.id < integration_pending_tasks.id
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
)
|
|
275
288
|
RETURNING id, platform, external_thread_id, payload, owner_email, org_id, status, attempts, error_message, created_at, updated_at, completed_at`
|
|
276
289
|
: `UPDATE integration_pending_tasks
|
|
277
290
|
SET status = ?, attempts = attempts + 1, updated_at = ?
|
|
@@ -282,6 +295,19 @@ export async function claimPendingTask(
|
|
|
282
295
|
AND active.external_thread_id = integration_pending_tasks.external_thread_id
|
|
283
296
|
AND active.status = 'processing'
|
|
284
297
|
AND active.id <> integration_pending_tasks.id
|
|
298
|
+
)
|
|
299
|
+
AND NOT EXISTS (
|
|
300
|
+
SELECT 1 FROM integration_pending_tasks earlier
|
|
301
|
+
WHERE earlier.platform = integration_pending_tasks.platform
|
|
302
|
+
AND earlier.external_thread_id = integration_pending_tasks.external_thread_id
|
|
303
|
+
AND earlier.status = 'pending'
|
|
304
|
+
AND (
|
|
305
|
+
earlier.created_at < integration_pending_tasks.created_at
|
|
306
|
+
OR (
|
|
307
|
+
earlier.created_at = integration_pending_tasks.created_at
|
|
308
|
+
AND earlier.id < integration_pending_tasks.id
|
|
309
|
+
)
|
|
310
|
+
)
|
|
285
311
|
)`,
|
|
286
312
|
args: ["processing", now, id],
|
|
287
313
|
});
|
|
@@ -312,7 +338,7 @@ export async function getNextPendingTaskIdForThread(
|
|
|
312
338
|
const { rows } = await getDbExec().execute({
|
|
313
339
|
sql: `SELECT id FROM integration_pending_tasks
|
|
314
340
|
WHERE platform = ? AND external_thread_id = ? AND status = 'pending'
|
|
315
|
-
ORDER BY created_at ASC LIMIT 1`,
|
|
341
|
+
ORDER BY created_at ASC, id ASC LIMIT 1`,
|
|
316
342
|
args: [platform, externalThreadId],
|
|
317
343
|
});
|
|
318
344
|
return rows[0]?.id ? String(rows[0].id) : null;
|
|
@@ -354,6 +380,77 @@ export async function markTaskRetryable(
|
|
|
354
380
|
});
|
|
355
381
|
}
|
|
356
382
|
|
|
383
|
+
export async function stageTaskDeliveryPayload(
|
|
384
|
+
id: string,
|
|
385
|
+
payload: string,
|
|
386
|
+
): Promise<void> {
|
|
387
|
+
await ensureTable();
|
|
388
|
+
const client = getDbExec();
|
|
389
|
+
const now = Date.now();
|
|
390
|
+
const result = await client.execute({
|
|
391
|
+
sql: `UPDATE integration_pending_tasks
|
|
392
|
+
SET payload = ?, updated_at = ?
|
|
393
|
+
WHERE id = ? AND status = 'processing'`,
|
|
394
|
+
args: [payload, now, id],
|
|
395
|
+
});
|
|
396
|
+
const affected = Number(
|
|
397
|
+
(result as { rowsAffected?: number }).rowsAffected ??
|
|
398
|
+
(result as { rowCount?: number }).rowCount ??
|
|
399
|
+
0,
|
|
400
|
+
);
|
|
401
|
+
if (affected === 0) {
|
|
402
|
+
throw new Error("Integration task is no longer claimable for delivery");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export async function markTaskDeliveryRetryable(
|
|
407
|
+
id: string,
|
|
408
|
+
payload: string,
|
|
409
|
+
errorMessage: string,
|
|
410
|
+
): Promise<void> {
|
|
411
|
+
await ensureTable();
|
|
412
|
+
const client = getDbExec();
|
|
413
|
+
const result = await client.execute({
|
|
414
|
+
sql: `UPDATE integration_pending_tasks
|
|
415
|
+
SET status = ?, payload = ?, updated_at = ?, error_message = ?
|
|
416
|
+
WHERE id = ? AND status = 'processing'`,
|
|
417
|
+
args: ["pending", payload, Date.now(), errorMessage.slice(0, 2000), id],
|
|
418
|
+
});
|
|
419
|
+
const affected = Number(
|
|
420
|
+
(result as { rowsAffected?: number }).rowsAffected ??
|
|
421
|
+
(result as { rowCount?: number }).rowCount ??
|
|
422
|
+
0,
|
|
423
|
+
);
|
|
424
|
+
if (affected === 0) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
"Integration task is no longer claimable for delivery retry",
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export async function failTaskDeliveryTransition(
|
|
432
|
+
id: string,
|
|
433
|
+
errorMessage: string,
|
|
434
|
+
): Promise<void> {
|
|
435
|
+
await ensureTable();
|
|
436
|
+
const result = await getDbExec().execute({
|
|
437
|
+
sql: `UPDATE integration_pending_tasks
|
|
438
|
+
SET status = ?, updated_at = ?, error_message = ?, payload = ?
|
|
439
|
+
WHERE id = ? AND status = 'processing'`,
|
|
440
|
+
args: ["failed", Date.now(), errorMessage.slice(0, 2000), "{}", id],
|
|
441
|
+
});
|
|
442
|
+
const affected = Number(
|
|
443
|
+
(result as { rowsAffected?: number }).rowsAffected ??
|
|
444
|
+
(result as { rowCount?: number }).rowCount ??
|
|
445
|
+
0,
|
|
446
|
+
);
|
|
447
|
+
if (affected === 0) {
|
|
448
|
+
throw new Error(
|
|
449
|
+
"Integration task delivery failure transition lost its race",
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
357
454
|
/** Mark a task as failed and stash an error message. */
|
|
358
455
|
export async function markTaskFailed(
|
|
359
456
|
id: string,
|
|
@@ -78,13 +78,16 @@ import {
|
|
|
78
78
|
import { startPendingTasksRetryJob } from "./pending-tasks-retry-job.js";
|
|
79
79
|
import {
|
|
80
80
|
claimPendingTask,
|
|
81
|
+
failTaskDeliveryTransition,
|
|
81
82
|
getNextPendingTaskIdForThread,
|
|
82
83
|
insertPendingTask,
|
|
83
84
|
isDuplicateEventError,
|
|
84
85
|
MAX_PENDING_TASK_ATTEMPTS,
|
|
85
86
|
markTaskCompleted,
|
|
87
|
+
markTaskDeliveryRetryable,
|
|
86
88
|
markTaskFailed,
|
|
87
89
|
markTaskRetryable,
|
|
90
|
+
stageTaskDeliveryPayload,
|
|
88
91
|
} from "./pending-tasks-store.js";
|
|
89
92
|
import {
|
|
90
93
|
claimNextComputerCommand,
|
|
@@ -142,12 +145,18 @@ import type {
|
|
|
142
145
|
IntegrationStatus,
|
|
143
146
|
IntegrationExecutionContext,
|
|
144
147
|
IncomingMessage,
|
|
148
|
+
PlatformDeliveryReceipt,
|
|
145
149
|
} from "./types.js";
|
|
146
150
|
import {
|
|
147
151
|
listIntegrationUsageBudgets,
|
|
148
152
|
saveIntegrationUsageBudget,
|
|
149
153
|
} from "./usage-budget-store.js";
|
|
150
|
-
import {
|
|
154
|
+
import {
|
|
155
|
+
handleWebhook,
|
|
156
|
+
processIntegrationTask,
|
|
157
|
+
recordIntegrationResponseDelivery,
|
|
158
|
+
type IntegrationResponseDeliveryTaskPayload,
|
|
159
|
+
} from "./webhook-handler.js";
|
|
151
160
|
|
|
152
161
|
type NitroPluginDef = (nitroApp: any) => void | Promise<void>;
|
|
153
162
|
|
|
@@ -1615,6 +1624,11 @@ export function createIntegrationsPlugin(
|
|
|
1615
1624
|
return { ok: true, skipped: "already-claimed-or-missing" };
|
|
1616
1625
|
}
|
|
1617
1626
|
|
|
1627
|
+
let deliveryRetryTransitionStarted = false;
|
|
1628
|
+
let deliveryRetryRecovery:
|
|
1629
|
+
| { payload: string; errorMessage: string }
|
|
1630
|
+
| undefined;
|
|
1631
|
+
let confirmedDeliveryRetryPayload: string | undefined;
|
|
1618
1632
|
try {
|
|
1619
1633
|
const adapter = adapterMap.get(task.platform);
|
|
1620
1634
|
if (!adapter) {
|
|
@@ -1622,7 +1636,7 @@ export function createIntegrationsPlugin(
|
|
|
1622
1636
|
setResponseStatus(event, 404);
|
|
1623
1637
|
return { error: "Unknown platform" };
|
|
1624
1638
|
}
|
|
1625
|
-
await runWithRequestContext(
|
|
1639
|
+
const processingResult = await runWithRequestContext(
|
|
1626
1640
|
{
|
|
1627
1641
|
userEmail: task.ownerEmail,
|
|
1628
1642
|
...(task.orgId ? { orgId: task.orgId } : {}),
|
|
@@ -1631,6 +1645,7 @@ export function createIntegrationsPlugin(
|
|
|
1631
1645
|
async () => {
|
|
1632
1646
|
const taskPayload = JSON.parse(task.payload) as
|
|
1633
1647
|
| IntegrationSystemNoticeTaskPayload
|
|
1648
|
+
| IntegrationResponseDeliveryTaskPayload
|
|
1634
1649
|
| { kind?: undefined };
|
|
1635
1650
|
if (taskPayload.kind === "system-notice") {
|
|
1636
1651
|
if (!adapter.sendSystemNotice) {
|
|
@@ -1657,13 +1672,59 @@ export function createIntegrationsPlugin(
|
|
|
1657
1672
|
);
|
|
1658
1673
|
return;
|
|
1659
1674
|
}
|
|
1675
|
+
if (taskPayload.kind === "response-delivery") {
|
|
1676
|
+
let receipt: void | PlatformDeliveryReceipt =
|
|
1677
|
+
taskPayload.deliveryReceipt;
|
|
1678
|
+
if (!receipt) {
|
|
1679
|
+
receipt = await adapter.sendResponse(
|
|
1680
|
+
taskPayload.message,
|
|
1681
|
+
taskPayload.incoming,
|
|
1682
|
+
{
|
|
1683
|
+
...(taskPayload.placeholderRef
|
|
1684
|
+
? { placeholderRef: taskPayload.placeholderRef }
|
|
1685
|
+
: {}),
|
|
1686
|
+
},
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
if (receipt?.status !== "delivered") {
|
|
1690
|
+
throw new Error(
|
|
1691
|
+
`${task.platform} response completed without delivery proof`,
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
const deliveredPayload = taskPayload.deliveryReceipt
|
|
1695
|
+
? taskPayload
|
|
1696
|
+
: {
|
|
1697
|
+
...taskPayload,
|
|
1698
|
+
deliveryReceipt: receipt,
|
|
1699
|
+
deliveredAt: new Date().toISOString(),
|
|
1700
|
+
};
|
|
1701
|
+
confirmedDeliveryRetryPayload =
|
|
1702
|
+
JSON.stringify(deliveredPayload);
|
|
1703
|
+
if (!taskPayload.deliveryReceipt) {
|
|
1704
|
+
deliveryRetryRecovery = {
|
|
1705
|
+
payload: confirmedDeliveryRetryPayload,
|
|
1706
|
+
errorMessage:
|
|
1707
|
+
"Provider delivery was confirmed but its receipt checkpoint failed",
|
|
1708
|
+
};
|
|
1709
|
+
await stageTaskDeliveryPayload(
|
|
1710
|
+
task.id,
|
|
1711
|
+
deliveryRetryRecovery.payload,
|
|
1712
|
+
);
|
|
1713
|
+
deliveryRetryRecovery = undefined;
|
|
1714
|
+
}
|
|
1715
|
+
await recordIntegrationResponseDelivery(
|
|
1716
|
+
deliveredPayload,
|
|
1717
|
+
receipt,
|
|
1718
|
+
);
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1660
1721
|
const resources = await loadResourcesForPrompt(
|
|
1661
1722
|
task.ownerEmail,
|
|
1662
1723
|
true,
|
|
1663
1724
|
options?.appId,
|
|
1664
1725
|
task.orgId,
|
|
1665
1726
|
);
|
|
1666
|
-
await processIntegrationTask(task, {
|
|
1727
|
+
const result = await processIntegrationTask(task, {
|
|
1667
1728
|
adapter,
|
|
1668
1729
|
systemPrompt: baseSystemPrompt + resources,
|
|
1669
1730
|
actions,
|
|
@@ -1674,8 +1735,22 @@ export function createIntegrationsPlugin(
|
|
|
1674
1735
|
ownerEmail: task.ownerEmail,
|
|
1675
1736
|
appId: options?.appId,
|
|
1676
1737
|
});
|
|
1738
|
+
if (result?.status === "delivery-pending") {
|
|
1739
|
+
deliveryRetryTransitionStarted = true;
|
|
1740
|
+
await markTaskDeliveryRetryable(
|
|
1741
|
+
task.id,
|
|
1742
|
+
JSON.stringify(result.payload),
|
|
1743
|
+
result.errorMessage,
|
|
1744
|
+
);
|
|
1745
|
+
return "delivery-retry" as const;
|
|
1746
|
+
}
|
|
1747
|
+
return "completed" as const;
|
|
1677
1748
|
},
|
|
1678
1749
|
);
|
|
1750
|
+
if (processingResult === "delivery-retry") {
|
|
1751
|
+
setResponseStatus(event, 202);
|
|
1752
|
+
return { ok: true, taskId, retrying: "response-delivery" };
|
|
1753
|
+
}
|
|
1679
1754
|
await markTaskCompleted(taskId);
|
|
1680
1755
|
const nextTaskId = await getNextPendingTaskIdForThread(
|
|
1681
1756
|
task.platform,
|
|
@@ -1711,7 +1786,57 @@ export function createIntegrationsPlugin(
|
|
|
1711
1786
|
const errorMessage = err?.message
|
|
1712
1787
|
? String(err.message).slice(0, 1000)
|
|
1713
1788
|
: "processor failed";
|
|
1714
|
-
if (
|
|
1789
|
+
if (deliveryRetryRecovery) {
|
|
1790
|
+
try {
|
|
1791
|
+
await markTaskDeliveryRetryable(
|
|
1792
|
+
taskId,
|
|
1793
|
+
deliveryRetryRecovery.payload,
|
|
1794
|
+
`${deliveryRetryRecovery.errorMessage}: ${errorMessage}`,
|
|
1795
|
+
);
|
|
1796
|
+
setResponseStatus(event, 202);
|
|
1797
|
+
return { ok: true, taskId, retrying: "response-delivery" };
|
|
1798
|
+
} catch (transitionError) {
|
|
1799
|
+
const transitionMessage =
|
|
1800
|
+
transitionError instanceof Error
|
|
1801
|
+
? transitionError.message
|
|
1802
|
+
: String(transitionError);
|
|
1803
|
+
await failTaskDeliveryTransition(
|
|
1804
|
+
taskId,
|
|
1805
|
+
`Could not safely checkpoint the delivery receipt: ${transitionMessage}`,
|
|
1806
|
+
).catch((failureTransitionError) => {
|
|
1807
|
+
console.error(
|
|
1808
|
+
"[integrations] Failed to contain delivery receipt transition failure:",
|
|
1809
|
+
failureTransitionError,
|
|
1810
|
+
);
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
} else if (confirmedDeliveryRetryPayload) {
|
|
1814
|
+
try {
|
|
1815
|
+
await markTaskDeliveryRetryable(
|
|
1816
|
+
taskId,
|
|
1817
|
+
confirmedDeliveryRetryPayload,
|
|
1818
|
+
`Provider delivery was confirmed but history persistence failed: ${errorMessage}`,
|
|
1819
|
+
);
|
|
1820
|
+
console.error("[integrations] process-task failure:", err);
|
|
1821
|
+
setResponseStatus(event, 202);
|
|
1822
|
+
return { ok: true, taskId, retrying: "response-delivery" };
|
|
1823
|
+
} catch (transitionError) {
|
|
1824
|
+
console.error(
|
|
1825
|
+
"[integrations] Failed to requeue confirmed delivery history:",
|
|
1826
|
+
transitionError,
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
} else if (deliveryRetryTransitionStarted) {
|
|
1830
|
+
await failTaskDeliveryTransition(
|
|
1831
|
+
taskId,
|
|
1832
|
+
`Could not safely checkpoint the delivery retry: ${errorMessage}`,
|
|
1833
|
+
).catch((transitionError) => {
|
|
1834
|
+
console.error(
|
|
1835
|
+
"[integrations] Failed to contain delivery retry transition failure:",
|
|
1836
|
+
transitionError,
|
|
1837
|
+
);
|
|
1838
|
+
});
|
|
1839
|
+
} else if (task.attempts >= MAX_PENDING_TASK_ATTEMPTS) {
|
|
1715
1840
|
await markTaskFailed(taskId, errorMessage);
|
|
1716
1841
|
} else {
|
|
1717
1842
|
await markTaskRetryable(taskId, errorMessage);
|