@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.
Files changed (34) hide show
  1. package/corpus/core/CHANGELOG.md +6 -0
  2. package/corpus/core/package.json +1 -1
  3. package/corpus/core/src/a2a/artifact-response.ts +32 -10
  4. package/corpus/core/src/integrations/a2a-continuation-processor.ts +107 -3
  5. package/corpus/core/src/integrations/pending-tasks-store.ts +98 -1
  6. package/corpus/core/src/integrations/plugin.ts +129 -4
  7. package/corpus/core/src/integrations/webhook-handler.ts +339 -36
  8. package/corpus/core/src/server/agent-chat-plugin.ts +93 -8
  9. package/dist/a2a/artifact-response.d.ts +5 -1
  10. package/dist/a2a/artifact-response.d.ts.map +1 -1
  11. package/dist/a2a/artifact-response.js +18 -12
  12. package/dist/a2a/artifact-response.js.map +1 -1
  13. package/dist/collab/struct-routes.d.ts +1 -1
  14. package/dist/integrations/a2a-continuation-processor.js +62 -5
  15. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  16. package/dist/integrations/pending-tasks-store.d.ts +3 -0
  17. package/dist/integrations/pending-tasks-store.d.ts.map +1 -1
  18. package/dist/integrations/pending-tasks-store.js +75 -1
  19. package/dist/integrations/pending-tasks-store.js.map +1 -1
  20. package/dist/integrations/plugin.d.ts.map +1 -1
  21. package/dist/integrations/plugin.js +81 -5
  22. package/dist/integrations/plugin.js.map +1 -1
  23. package/dist/integrations/webhook-handler.d.ts +23 -2
  24. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  25. package/dist/integrations/webhook-handler.js +257 -35
  26. package/dist/integrations/webhook-handler.js.map +1 -1
  27. package/dist/notifications/routes.d.ts +2 -2
  28. package/dist/resources/handlers.d.ts +1 -1
  29. package/dist/server/agent-chat-plugin.d.ts +6 -0
  30. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  31. package/dist/server/agent-chat-plugin.js +71 -6
  32. package/dist/server/agent-chat-plugin.js.map +1 -1
  33. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  34. package/package.json +1 -1
@@ -1,5 +1,11 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.107.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 2a4d2db: Retry messaging replies without repeating successful agent mutations, and deliver verified A2A artifact checkpoints as soon as they are durable.
8
+
3
9
  ## 0.107.1
4
10
 
5
11
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.107.1",
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
- const secret = process.env.A2A_SECRET;
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
- if (
89
- supplied.length !== expected.length ||
90
- !timingSafeEqual(supplied, expected)
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 identities = extractA2AArtifactIdentities(toolResults).slice(0, 12);
119
- const secret = process.env.A2A_SECRET;
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(value, toolResults)
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 { handleWebhook, processIntegrationTask } from "./webhook-handler.js";
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 (task.attempts >= MAX_PENDING_TASK_ATTEMPTS) {
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);