@agent-native/core 0.130.0 → 0.130.1

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 (63) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/integrations/a2a-continuation-processor.ts +481 -122
  5. package/corpus/core/src/integrations/a2a-continuations-store.ts +345 -14
  6. package/corpus/core/src/integrations/adapters/slack.ts +243 -53
  7. package/corpus/core/src/integrations/integration-campaign-recovery.ts +4 -1
  8. package/corpus/core/src/integrations/plugin.ts +39 -21
  9. package/corpus/core/src/integrations/task-queue-stats.ts +146 -0
  10. package/corpus/core/src/integrations/types.ts +25 -4
  11. package/corpus/core/src/integrations/webhook-handler.ts +18 -5
  12. package/corpus/templates/dispatch/changelog/2026-07-26-slack-replies-recover-after-connected-app-updates.md +6 -0
  13. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +15 -0
  14. package/corpus/templates/slides/actions/delete-design-system.ts +145 -0
  15. package/corpus/templates/slides/actions/list-design-systems.ts +94 -2
  16. package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +43 -1
  17. package/corpus/templates/slides/app/hooks/use-design-systems.ts +2 -0
  18. package/corpus/templates/slides/app/i18n/en-US.ts +7 -0
  19. package/corpus/templates/slides/app/pages/DesignSystems.tsx +56 -1
  20. package/corpus/templates/slides/changelog/2026-07-28-delete-a-design-system-you-own-from-the-design-systems-page-.md +6 -0
  21. package/dist/collab/struct-routes.d.ts +1 -1
  22. package/dist/integrations/a2a-continuation-processor.d.ts +5 -0
  23. package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
  24. package/dist/integrations/a2a-continuation-processor.js +293 -79
  25. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  26. package/dist/integrations/a2a-continuations-store.d.ts +23 -1
  27. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  28. package/dist/integrations/a2a-continuations-store.js +249 -15
  29. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  30. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  31. package/dist/integrations/adapters/slack.js +155 -31
  32. package/dist/integrations/adapters/slack.js.map +1 -1
  33. package/dist/integrations/integration-campaign-recovery.d.ts.map +1 -1
  34. package/dist/integrations/integration-campaign-recovery.js +3 -1
  35. package/dist/integrations/integration-campaign-recovery.js.map +1 -1
  36. package/dist/integrations/plugin.d.ts.map +1 -1
  37. package/dist/integrations/plugin.js +29 -12
  38. package/dist/integrations/plugin.js.map +1 -1
  39. package/dist/integrations/task-queue-stats.d.ts +12 -0
  40. package/dist/integrations/task-queue-stats.d.ts.map +1 -1
  41. package/dist/integrations/task-queue-stats.js +106 -0
  42. package/dist/integrations/task-queue-stats.js.map +1 -1
  43. package/dist/integrations/types.d.ts +24 -6
  44. package/dist/integrations/types.d.ts.map +1 -1
  45. package/dist/integrations/types.js.map +1 -1
  46. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  47. package/dist/integrations/webhook-handler.js +12 -3
  48. package/dist/integrations/webhook-handler.js.map +1 -1
  49. package/dist/observability/routes.d.ts +3 -3
  50. package/dist/progress/routes.d.ts +1 -1
  51. package/dist/resources/handlers.d.ts +1 -1
  52. package/dist/secrets/routes.d.ts +9 -9
  53. package/dist/server/realtime-token.d.ts +1 -1
  54. package/dist/server/transcribe-voice.d.ts +1 -1
  55. package/package.json +1 -1
  56. package/src/integrations/a2a-continuation-processor.ts +481 -122
  57. package/src/integrations/a2a-continuations-store.ts +345 -14
  58. package/src/integrations/adapters/slack.ts +243 -53
  59. package/src/integrations/integration-campaign-recovery.ts +4 -1
  60. package/src/integrations/plugin.ts +39 -21
  61. package/src/integrations/task-queue-stats.ts +146 -0
  62. package/src/integrations/types.ts +25 -4
  63. package/src/integrations/webhook-handler.ts +18 -5
@@ -5,8 +5,8 @@ import { extractThreadMeta } from "../agent/thread-data-builder.js";
5
5
  import { getThread, updateThreadData } from "../chat-threads/store.js";
6
6
  import { withConfiguredAppBasePath } from "../server/app-base-path.js";
7
7
  import { FRAMEWORK_ROUTE_PREFIX } from "../server/core-routes-plugin.js";
8
- import { claimA2AContinuation, claimA2AContinuationDelivery, claimDueA2AContinuations, completeA2AContinuation, failA2AContinuationsForIntegrationTask, failA2AContinuation, getA2AContinuation, hasActiveA2AContinuationsForIntegrationTask, listRecoverableA2AIntegrationTasks, recoverDueA2AContinuationIds, rescheduleA2AContinuation, } from "./a2a-continuations-store.js";
9
- import { completeIntegrationCampaignTaskAfterA2A, failDisabledIntegrationCampaignTask, getIntegrationCampaignForTask, } from "./integration-campaigns-store.js";
8
+ import { claimA2AContinuation, claimA2AContinuationDelivery, claimDueA2AContinuations, failA2AContinuationsForIntegrationTask, failA2AContinuation, finalizeA2ATerminalHistory, getA2AContinuation, getA2AContinuationTaskOutcome, hasOnlyLegacyFailedA2AContinuationsForIntegrationTask, hasPendingConfirmedA2ADeliveryForIntegrationTask, listRecoverableA2AIntegrationTasks, recoverDueA2AContinuationIds, recordA2ATerminalDeliveryReceipt, retainA2AUnconfirmedDeliveryClaim, rescheduleA2AContinuation, saveA2AVerifiedArtifactCheckpoint, } from "./a2a-continuations-store.js";
9
+ import { completeIntegrationCampaignTaskAfterA2A, failDisabledIntegrationCampaignTask, failIntegrationCampaignTaskDeliveryContainment, getIntegrationCampaignForTask, } from "./integration-campaigns-store.js";
10
10
  import { dispatchPendingIntegrationTask, isIntegrationDurableDispatchEnabledForTask, } from "./integration-durable-dispatch.js";
11
11
  import { signInternalToken } from "./internal-token.js";
12
12
  import { getNextPendingTaskForThread, getPendingTask, } from "./pending-tasks-store.js";
@@ -26,6 +26,16 @@ const POLL_REQUEST_TIMEOUT_MS = 8_000;
26
26
  const PLATFORM_SEND_TIMEOUT_MS = 12_000;
27
27
  const DISPATCH_SETTLE_WAIT_MS = 2_000;
28
28
  const COMPLETE_AFTER_DELIVERY_ATTEMPTS = 3;
29
+ function logA2AContinuationTransition(event, continuation) {
30
+ console.info("[integrations] a2a-continuation-transition", {
31
+ event,
32
+ continuationId: continuation.id,
33
+ integrationTaskId: continuation.integrationTaskId,
34
+ downstreamTaskId: continuation.a2aTaskId,
35
+ attempt: continuation.attempts,
36
+ status: continuation.status,
37
+ });
38
+ }
29
39
  export async function dispatchA2AContinuation(continuationId, webhookBaseUrl) {
30
40
  const baseUrl = webhookBaseUrl ||
31
41
  process.env.WEBHOOK_BASE_URL ||
@@ -87,10 +97,74 @@ export async function processA2AContinuationById(continuationId, options) {
87
97
  return;
88
98
  await processClaimedContinuation(continuation, options);
89
99
  }
100
+ export async function recoverA2AContinuationAfterProcessorFailure(continuationId, options) {
101
+ const continuation = await getA2AContinuation(continuationId);
102
+ if (!continuation ||
103
+ continuation.status === "completed" ||
104
+ continuation.status === "failed") {
105
+ return;
106
+ }
107
+ if (continuation.terminalDeliveryConfirmedAt != null &&
108
+ continuation.terminalHistoryPayload) {
109
+ await persistAndFinalizeConfirmedA2ADelivery(continuation);
110
+ return;
111
+ }
112
+ const adapter = options.adapters.get(continuation.platform);
113
+ if (continuation.attempts < MAX_ATTEMPTS) {
114
+ logA2AContinuationTransition("processor_released", continuation);
115
+ await rescheduleAndRedispatchA2AContinuation(continuation.id);
116
+ return;
117
+ }
118
+ if (!adapter) {
119
+ const reason = `Unknown platform: ${continuation.platform}`;
120
+ logA2AContinuationTransition("processor_exhausted_without_adapter", continuation);
121
+ await failA2AContinuationsForIntegrationTask(continuation.integrationTaskId, reason);
122
+ if (await hasPendingConfirmedA2ADeliveryForIntegrationTask(continuation.integrationTaskId)) {
123
+ await dispatchPendingIntegrationTask({
124
+ taskId: continuation.integrationTaskId,
125
+ task: {
126
+ platform: continuation.platform,
127
+ externalThreadId: continuation.externalThreadId,
128
+ platformContext: continuation.incoming.platformContext,
129
+ },
130
+ campaignContinuation: true,
131
+ allowPortableConfirmedReceiptReconciliation: true,
132
+ }).catch((err) => {
133
+ console.error(`[integrations] Failed to wake confirmed sibling history for ${continuation.integrationTaskId}:`, err);
134
+ });
135
+ return;
136
+ }
137
+ await failIntegrationCampaignTaskDeliveryContainment(continuation.integrationTaskId, reason);
138
+ return;
139
+ }
140
+ const progress = await resumeA2AContinuationProgress(continuation, adapter);
141
+ if (continuation.verifiedArtifactCheckpoint) {
142
+ logA2AContinuationTransition("processor_exhausted_checkpoint_delivery", continuation);
143
+ await deliverAndCompleteA2AContinuation(continuation, adapter, formatRecoverableArtifactFallbackText(continuation.verifiedArtifactCheckpoint), progress);
144
+ return;
145
+ }
146
+ logA2AContinuationTransition("processor_exhausted_failure_notice", continuation);
147
+ await notifyAndFailA2AContinuation(continuation, adapter, options.reason, progress);
148
+ }
90
149
  export async function processDueA2AContinuations(options) {
91
150
  const continuations = await claimDueA2AContinuations(options.limit ?? 5);
92
151
  for (const continuation of continuations) {
93
- await processClaimedContinuation(continuation, options).catch((err) => console.error(`[integrations] A2A continuation ${continuation.id} failed:`, err));
152
+ await processClaimedContinuation(continuation, options).catch(async (err) => {
153
+ console.error(`[integrations] A2A continuation ${continuation.id} failed; durable recovery requested`);
154
+ try {
155
+ await recoverA2AContinuationAfterProcessorFailure(continuation.id, {
156
+ adapters: options.adapters,
157
+ reason: err instanceof Error
158
+ ? err.message.slice(0, 500)
159
+ : "continuation processing failed",
160
+ });
161
+ }
162
+ catch (recoveryError) {
163
+ console.error(`[integrations] A2A continuation ${continuation.id} recovery failed; later continuations will continue`, recoveryError instanceof Error
164
+ ? recoveryError.name
165
+ : "recovery_error");
166
+ }
167
+ });
94
168
  }
95
169
  }
96
170
  /**
@@ -104,6 +178,7 @@ export async function recoverDueA2AContinuations(options) {
104
178
  const limit = options?.limit ?? 5;
105
179
  const candidateTasks = await listRecoverableA2AIntegrationTasks(200);
106
180
  const eligibleTaskIds = [];
181
+ const confirmedHistoryTaskIds = [];
107
182
  for (const task of candidateTasks) {
108
183
  const enabled = isIntegrationDurableDispatchEnabledForTask({
109
184
  platform: task.platform,
@@ -114,14 +189,22 @@ export async function recoverDueA2AContinuations(options) {
114
189
  });
115
190
  if (enabled) {
116
191
  eligibleTaskIds.push(task.id);
117
- if (eligibleTaskIds.length >= limit)
118
- break;
192
+ }
193
+ else if (task.hasPendingConfirmedDelivery) {
194
+ confirmedHistoryTaskIds.push(task.id);
119
195
  }
120
196
  else {
121
197
  await failDisabledDurableA2ATask(task);
122
198
  }
199
+ if (eligibleTaskIds.length + confirmedHistoryTaskIds.length >= limit)
200
+ break;
123
201
  }
124
202
  const ids = await recoverDueA2AContinuationIds(limit, eligibleTaskIds);
203
+ const remaining = Math.max(0, limit - ids.length);
204
+ const confirmedHistoryIds = remaining > 0 && confirmedHistoryTaskIds.length > 0
205
+ ? await recoverDueA2AContinuationIds(remaining, confirmedHistoryTaskIds, true)
206
+ : [];
207
+ ids.push(...confirmedHistoryIds);
125
208
  let dispatched = 0;
126
209
  let failed = 0;
127
210
  await Promise.all(ids.map(async (id) => {
@@ -137,6 +220,11 @@ export async function recoverDueA2AContinuations(options) {
137
220
  return { dispatched, failed };
138
221
  }
139
222
  async function processClaimedContinuation(continuation, options) {
223
+ if (continuation.terminalDeliveryConfirmedAt != null &&
224
+ continuation.terminalHistoryPayload) {
225
+ await persistAndFinalizeConfirmedA2ADelivery(continuation);
226
+ return;
227
+ }
140
228
  if (!(await durableContinuationScopeStillEnabled(continuation)))
141
229
  return;
142
230
  const adapter = options.adapters.get(continuation.platform);
@@ -153,13 +241,16 @@ async function processClaimedContinuation(continuation, options) {
153
241
  const deadline = Date.now() + PROCESSOR_WAIT_MS;
154
242
  const recoverableArtifactSecrets = await resolveContinuationArtifactSecrets(continuation);
155
243
  let task = null;
156
- let latestRecoverableArtifactText = null;
244
+ let latestRecoverableArtifactText = continuation.verifiedArtifactCheckpoint;
157
245
  try {
158
246
  while (Date.now() < deadline) {
159
247
  task = await client.getTask(continuation.a2aTaskId);
160
248
  const recoverableArtifactText = extractVerifiedRecoverableArtifactText(task, continuation.agentUrl, recoverableArtifactSecrets);
161
249
  if (recoverableArtifactText) {
162
- latestRecoverableArtifactText = recoverableArtifactText;
250
+ latestRecoverableArtifactText = await saveA2AVerifiedArtifactCheckpoint(continuation.id, recoverableArtifactText);
251
+ if (latestRecoverableArtifactText) {
252
+ logA2AContinuationTransition("checkpoint_persisted", continuation);
253
+ }
163
254
  }
164
255
  if (TERMINAL_STATES.has(task.status.state))
165
256
  break;
@@ -181,6 +272,10 @@ async function processClaimedContinuation(continuation, options) {
181
272
  return;
182
273
  }
183
274
  if (continuation.attempts >= MAX_ATTEMPTS) {
275
+ if (latestRecoverableArtifactText) {
276
+ await deliverAndCompleteA2AContinuation(continuation, adapter, formatRecoverableArtifactFallbackText(latestRecoverableArtifactText), progress);
277
+ return;
278
+ }
184
279
  await notifyAndFailA2AContinuation(continuation, adapter, err instanceof Error ? err.message : String(err), progress);
185
280
  return;
186
281
  }
@@ -239,6 +334,11 @@ async function durableContinuationScopeStillEnabled(continuation) {
239
334
  });
240
335
  if (enabled)
241
336
  return true;
337
+ if (await hasPendingConfirmedA2ADeliveryForIntegrationTask(continuation.integrationTaskId)) {
338
+ await failA2AContinuation(continuation.id, "Durable integration campaign was disabled before this continuation delivered");
339
+ await reconcileTerminalA2AParentIfDisabled(continuation.integrationTaskId);
340
+ return false;
341
+ }
242
342
  await failDisabledDurableA2ATask({
243
343
  id: continuation.integrationTaskId,
244
344
  platform: task?.platform ?? continuation.platform,
@@ -266,6 +366,35 @@ async function failDisabledDurableA2ATask(task) {
266
366
  });
267
367
  }
268
368
  }
369
+ export async function reconcileTerminalA2AParentIfDisabled(integrationTaskId) {
370
+ if ((await getA2AContinuationTaskOutcome(integrationTaskId)) !==
371
+ "terminal-without-delivery") {
372
+ return false;
373
+ }
374
+ if (await hasOnlyLegacyFailedA2AContinuationsForIntegrationTask(integrationTaskId)) {
375
+ await failIntegrationCampaignTaskDeliveryContainment(integrationTaskId, "Legacy A2A continuation ended without durable delivery proof");
376
+ return true;
377
+ }
378
+ const task = await getPendingTask(integrationTaskId);
379
+ if (!task ||
380
+ isIntegrationDurableDispatchEnabledForTask({
381
+ platform: task.platform,
382
+ externalThreadId: task.externalThreadId,
383
+ platformContext: task.dispatchScope
384
+ ? { channelId: task.dispatchScope }
385
+ : undefined,
386
+ })) {
387
+ return false;
388
+ }
389
+ await failDisabledDurableA2ATask({
390
+ id: task.id,
391
+ platform: task.platform,
392
+ externalThreadId: task.externalThreadId,
393
+ dispatchScope: task.dispatchScope,
394
+ status: task.status,
395
+ });
396
+ return true;
397
+ }
269
398
  async function resumeA2AContinuationProgress(continuation, adapter) {
270
399
  if (!continuation.progressRef || !adapter.resumeRunProgress)
271
400
  return null;
@@ -322,23 +451,23 @@ async function notifyAndFailA2AContinuation(continuation, adapter, reason, progr
322
451
  const deliveryContinuation = await claimA2AContinuationDelivery(continuation.id);
323
452
  if (!deliveryContinuation)
324
453
  return;
454
+ logA2AContinuationTransition("failure_delivery_claimed", deliveryContinuation);
325
455
  const message = formatContinuationFailureMessage(deliveryContinuation, reason);
456
+ let outgoing;
457
+ let deliveryReceipt;
326
458
  try {
327
- const outgoing = adapter.formatAgentResponse(message);
328
- await withTimeout(deliverA2AContinuationResponse(adapter, deliveryContinuation, outgoing, progress, "error"), PLATFORM_SEND_TIMEOUT_MS, `${deliveryContinuation.platform} failure notification timed out`);
459
+ outgoing = adapter.formatAgentResponse(message);
460
+ deliveryReceipt = await withTimeout((signal) => deliverA2AContinuationResponse(adapter, deliveryContinuation, outgoing, progress, "error", signal), PLATFORM_SEND_TIMEOUT_MS, `${deliveryContinuation.platform} failure notification timed out`);
329
461
  }
330
462
  catch (err) {
331
463
  console.error(`[integrations] Failed to notify ${deliveryContinuation.platform} about failed A2A continuation ${deliveryContinuation.id}:`, err);
332
- if (deliveryContinuation.attempts >= MAX_ATTEMPTS) {
333
- await failA2AContinuation(deliveryContinuation.id, reason);
334
- await completeParentCampaignAfterTerminalA2A(deliveryContinuation);
335
- return;
336
- }
337
464
  await rescheduleAndRedispatchA2AContinuation(deliveryContinuation.id);
338
465
  return;
339
466
  }
340
- await failA2AContinuation(deliveryContinuation.id, reason);
341
- await completeParentCampaignAfterTerminalA2A(deliveryContinuation);
467
+ const confirmed = await recordTerminalA2ADelivery(deliveryContinuation, "failure", outgoing, deliveryReceipt, [], reason);
468
+ if (!confirmed)
469
+ return;
470
+ await persistAndFinalizeConfirmedA2ADelivery(confirmed);
342
471
  }
343
472
  async function deliverAndCompleteA2AContinuation(continuation, adapter, text, progress = null) {
344
473
  if (!(await durableContinuationScopeStillEnabled(continuation)))
@@ -346,77 +475,76 @@ async function deliverAndCompleteA2AContinuation(continuation, adapter, text, pr
346
475
  const deliveryContinuation = await claimA2AContinuationDelivery(continuation.id);
347
476
  if (!deliveryContinuation)
348
477
  return;
478
+ logA2AContinuationTransition("response_delivery_claimed", deliveryContinuation);
479
+ let outgoing;
480
+ let deliveryReceipt;
481
+ const artifactSecrets = await resolveContinuationArtifactSecrets(deliveryContinuation);
482
+ const artifacts = extractA2AArtifactIdentities([{ tool: "call-agent", result: text }], { persistedArtifactSecrets: artifactSecrets });
349
483
  try {
350
- const outgoing = adapter.formatAgentResponse(stripA2APersistedArtifactMarkers(text));
351
- const deliveryReceipt = await withTimeout(deliverA2AContinuationResponse(adapter, deliveryContinuation, outgoing, progress, "done"), PLATFORM_SEND_TIMEOUT_MS, `${deliveryContinuation.platform} response delivery timed out`);
352
- let persistenceError;
353
- const artifactSecrets = await resolveContinuationArtifactSecrets(deliveryContinuation);
354
- for (let attempt = 0; attempt < 3; attempt += 1) {
355
- try {
356
- await persistA2AContinuationDelivery(deliveryContinuation, outgoing, deliveryReceipt, text, artifactSecrets);
357
- persistenceError = undefined;
358
- break;
359
- }
360
- catch (err) {
361
- persistenceError = err;
362
- }
363
- }
364
- if (persistenceError) {
365
- console.error(`[integrations] Delivered A2A continuation ${deliveryContinuation.id} but could not persist its thread history:`, persistenceError);
366
- }
367
- }
368
- catch (err) {
369
- if (deliveryContinuation.attempts >= MAX_ATTEMPTS) {
370
- await failA2AContinuation(deliveryContinuation.id, err instanceof Error ? err.message : String(err));
484
+ outgoing = adapter.formatAgentResponse(stripA2APersistedArtifactMarkers(text));
485
+ deliveryReceipt = await withTimeout((signal) => deliverA2AContinuationResponse(adapter, deliveryContinuation, outgoing, progress, "done", signal), PLATFORM_SEND_TIMEOUT_MS, `${deliveryContinuation.platform} response delivery timed out`);
486
+ const confirmed = await recordTerminalA2ADelivery(deliveryContinuation, "success", outgoing, deliveryReceipt, artifacts);
487
+ if (!confirmed)
371
488
  return;
372
- }
489
+ await persistAndFinalizeConfirmedA2ADelivery(confirmed);
490
+ }
491
+ catch {
373
492
  await rescheduleAndRedispatchA2AContinuation(deliveryContinuation.id);
374
493
  return;
375
494
  }
376
- await completeAfterSuccessfulDelivery(deliveryContinuation);
377
495
  }
378
- async function deliverA2AContinuationResponse(adapter, continuation, message, progress, status) {
496
+ async function deliverA2AContinuationResponse(adapter, continuation, message, progress, status, signal) {
379
497
  if (progress) {
380
498
  try {
381
499
  await progress.onEvent({
382
500
  type: "agent_call",
383
501
  agent: continuation.agentName,
384
502
  status,
385
- });
386
- const receipt = await progress.complete(message);
503
+ }, { signal });
504
+ throwIfAborted(signal);
505
+ const receipt = await progress.complete(message, { signal });
387
506
  if (receipt?.status === "delivered")
388
507
  return receipt;
389
508
  throw new Error("Continuation progress completed without delivery proof");
390
509
  }
391
510
  catch {
511
+ throwIfAborted(signal);
392
512
  // A resumed Slack stream can no longer be finalized (for example when
393
513
  // chat.stopStream rejects). Preserve the final answer with the same
394
514
  // thread reply fallback used by the initial webhook run. Also ask the
395
515
  // adapter to terminate the native stream: otherwise Slack can keep the
396
516
  // task card in its working state after the thread fallback succeeds.
397
517
  try {
398
- await progress.fail?.("I couldn't update the live response, but I posted the final result in this thread.");
518
+ await progress.fail?.("I couldn't update the live response, but I posted the final result in this thread.", { signal });
399
519
  }
400
520
  catch {
401
521
  // The thread reply below is still the authoritative final answer.
402
522
  }
523
+ logA2AContinuationTransition("native_progress_fallback", continuation);
403
524
  }
404
525
  }
526
+ throwIfAborted(signal);
405
527
  const receipt = await adapter.sendResponse(message, continuation.incoming, {
406
- placeholderRef: continuation.placeholderRef ?? undefined,
528
+ idempotencyKey: `a2a-continuation:${continuation.id}`,
529
+ reconcileAfter: continuation.createdAt,
530
+ signal,
531
+ placeholderRef: progress?.responseTargetRef ?? continuation.placeholderRef ?? undefined,
532
+ strictTargetRef: true,
407
533
  });
408
534
  if (receipt?.status !== "delivered") {
409
535
  throw new Error("Continuation response completed without delivery proof");
410
536
  }
411
537
  return receipt;
412
538
  }
413
- async function persistA2AContinuationDelivery(continuation, outgoing, receipt, artifactText, artifactSecrets) {
539
+ async function persistA2AContinuationDelivery(continuation, history) {
414
540
  const mapping = await getThreadMapping(continuation.platform, continuation.externalThreadId);
415
- if (!mapping)
416
- return;
541
+ if (!mapping) {
542
+ throw new Error("Integration thread mapping is not available");
543
+ }
417
544
  const thread = await getThread(mapping.internalThreadId);
418
- if (!thread)
419
- return;
545
+ if (!thread) {
546
+ throw new Error("Integration chat thread is not available");
547
+ }
420
548
  let repo;
421
549
  try {
422
550
  repo = JSON.parse(thread.threadData || "{}");
@@ -426,30 +554,31 @@ async function persistA2AContinuationDelivery(continuation, outgoing, receipt, a
426
554
  }
427
555
  if (!Array.isArray(repo.messages))
428
556
  repo.messages = [];
429
- const artifacts = extractA2AArtifactIdentities([{ tool: "call-agent", result: artifactText }], {
430
- persistedArtifactSecrets: artifactSecrets,
431
- });
432
557
  const metadata = {
433
558
  integrationDeliveryAttempted: true,
434
559
  integrationDelivery: {
435
560
  platform: continuation.platform,
436
561
  status: "delivered",
437
- text: outgoing.text,
438
- deliveredAt: new Date().toISOString(),
439
- ...(receipt.messageRefs?.length
440
- ? { messageRefs: receipt.messageRefs }
562
+ text: history.text,
563
+ deliveredAt: history.deliveredAt,
564
+ ...(history.messageRefs.length
565
+ ? { messageRefs: history.messageRefs }
441
566
  : {}),
442
567
  },
443
568
  };
444
- if (artifacts.length > 0)
445
- metadata.integrationArtifacts = artifacts;
446
- repo.messages.push({
447
- id: `msg-${Date.now()}-assistant-continuation`,
448
- role: "assistant",
449
- content: [{ type: "text", text: outgoing.text }],
450
- createdAt: new Date().toISOString(),
451
- metadata,
452
- });
569
+ if (history.artifacts.length > 0) {
570
+ metadata.integrationArtifacts = history.artifacts;
571
+ }
572
+ const messageId = `msg-${continuation.id}-assistant-continuation`;
573
+ if (!repo.messages.some((message) => message?.id === messageId)) {
574
+ repo.messages.push({
575
+ id: messageId,
576
+ role: "assistant",
577
+ content: [{ type: "text", text: history.text }],
578
+ createdAt: history.deliveredAt,
579
+ metadata,
580
+ });
581
+ }
453
582
  const meta = extractThreadMeta(repo);
454
583
  await updateThreadData(mapping.internalThreadId, JSON.stringify(repo), meta.title || thread.title || "Integration Chat", meta.preview || thread.preview || "", repo.messages.length);
455
584
  }
@@ -459,31 +588,100 @@ async function rescheduleAndRedispatchA2AContinuation(continuationId) {
459
588
  console.error(`[integrations] Failed to redispatch A2A continuation ${continuationId}:`, err);
460
589
  });
461
590
  }
462
- async function completeAfterSuccessfulDelivery(continuation) {
463
- let lastError;
591
+ async function recordTerminalA2ADelivery(continuation, kind, outgoing, receipt, artifacts, errorMessage) {
592
+ const historyPayload = {
593
+ text: outgoing.text,
594
+ deliveredAt: new Date().toISOString(),
595
+ messageRefs: receipt.messageRefs ?? [],
596
+ artifacts,
597
+ };
464
598
  for (let attempt = 0; attempt < COMPLETE_AFTER_DELIVERY_ATTEMPTS; attempt++) {
465
599
  try {
466
- await completeA2AContinuation(continuation.id);
467
- await completeParentCampaignAfterTerminalA2A(continuation);
600
+ const confirmed = await recordA2ATerminalDeliveryReceipt(continuation.id, kind, historyPayload, errorMessage);
601
+ logA2AContinuationTransition(kind === "success"
602
+ ? "response_delivery_confirmed"
603
+ : "failure_delivery_confirmed", continuation);
604
+ return confirmed;
605
+ }
606
+ catch { }
607
+ }
608
+ console.error(`[integrations] ${continuation.platform} accepted terminal A2A delivery for ${continuation.id}, ` +
609
+ `but recording its receipt failed after ${COMPLETE_AFTER_DELIVERY_ATTEMPTS} attempts. Leaving it in delivering for stale recovery.`);
610
+ try {
611
+ await retainA2AUnconfirmedDeliveryClaim(continuation.id);
612
+ }
613
+ catch (err) {
614
+ console.error(`[integrations] Failed to retain unconfirmed A2A delivery claim ${continuation.id}:`, err instanceof Error ? err.name : "receipt_recovery_error");
615
+ }
616
+ return null;
617
+ }
618
+ async function persistAndFinalizeConfirmedA2ADelivery(continuation) {
619
+ const history = continuation.terminalHistoryPayload;
620
+ if (!history || continuation.terminalDeliveryConfirmedAt == null) {
621
+ throw new Error("Confirmed A2A delivery is missing durable history");
622
+ }
623
+ let lastError;
624
+ let historyFinalized = false;
625
+ for (let attempt = 0; attempt < 3; attempt += 1) {
626
+ try {
627
+ if (!historyFinalized) {
628
+ await persistA2AContinuationDelivery(continuation, history);
629
+ await finalizeA2ATerminalHistory(continuation.id);
630
+ historyFinalized = true;
631
+ logA2AContinuationTransition("terminal_history_persisted", continuation);
632
+ }
633
+ await completeParentCampaignAfterTerminalA2A({
634
+ ...continuation,
635
+ status: continuation.terminalDeliveryKind === "success"
636
+ ? "completed"
637
+ : "failed",
638
+ });
468
639
  return;
469
640
  }
470
641
  catch (err) {
471
642
  lastError = err;
472
643
  }
473
644
  }
474
- console.error(`[integrations] ${continuation.platform} accepted A2A continuation ${continuation.id}, ` +
475
- "but marking it completed failed. Leaving it in delivering for stale-delivery recovery.", lastError);
645
+ console.error(historyFinalized
646
+ ? `[integrations] A2A continuation ${continuation.id} finalized history but parent completion remains retryable:`
647
+ : `[integrations] A2A continuation ${continuation.id} has a provider receipt but its history remains retryable:`, lastError instanceof Error ? lastError.name : "persistence_error");
648
+ if (historyFinalized) {
649
+ await dispatchPendingIntegrationTask({
650
+ taskId: continuation.integrationTaskId,
651
+ task: {
652
+ platform: continuation.platform,
653
+ externalThreadId: continuation.externalThreadId,
654
+ platformContext: continuation.incoming.platformContext,
655
+ },
656
+ campaignContinuation: true,
657
+ allowPortableConfirmedReceiptReconciliation: true,
658
+ }).catch((err) => {
659
+ console.error(`[integrations] Failed to wake A2A parent ${continuation.integrationTaskId} after terminal history finalization:`, err);
660
+ });
661
+ return;
662
+ }
663
+ await rescheduleAndRedispatchA2AContinuation(continuation.id);
476
664
  }
477
665
  async function completeParentCampaignAfterTerminalA2A(continuation) {
478
666
  const campaign = await getIntegrationCampaignForTask(continuation.integrationTaskId);
479
667
  if (!campaign)
480
668
  return;
481
- if (await hasActiveA2AContinuationsForIntegrationTask(continuation.integrationTaskId)) {
669
+ const outcome = await getA2AContinuationTaskOutcome(continuation.integrationTaskId);
670
+ if (outcome !== "terminal-delivered") {
671
+ if (outcome === "terminal-without-delivery") {
672
+ if (await reconcileTerminalA2AParentIfDisabled(continuation.integrationTaskId)) {
673
+ return;
674
+ }
675
+ }
676
+ if (outcome !== "active") {
677
+ console.warn(`[integrations] Refusing to complete A2A parent ${continuation.integrationTaskId} with outcome ${outcome}`);
678
+ }
482
679
  return;
483
680
  }
484
681
  const completed = await completeIntegrationCampaignTaskAfterA2A(continuation.integrationTaskId);
485
682
  if (!completed)
486
683
  return;
684
+ logA2AContinuationTransition("parent_completed", continuation);
487
685
  const nextTask = await getNextPendingTaskForThread(continuation.platform, continuation.externalThreadId);
488
686
  if (!nextTask)
489
687
  return;
@@ -550,21 +748,37 @@ function remotePollFailureReason(continuation) {
550
748
  function sleep(ms) {
551
749
  return new Promise((resolve) => setTimeout(resolve, ms));
552
750
  }
553
- async function withTimeout(promise, timeoutMs, message) {
751
+ async function withTimeout(operation, timeoutMs, message) {
752
+ const controller = new AbortController();
554
753
  let timer;
754
+ let timedOut = false;
555
755
  try {
556
- return await Promise.race([
557
- promise,
558
- new Promise((_, reject) => {
559
- timer = setTimeout(() => reject(new Error(message)), timeoutMs);
560
- }),
561
- ]);
756
+ timer = setTimeout(() => {
757
+ timedOut = true;
758
+ controller.abort(new Error(message));
759
+ }, timeoutMs);
760
+ const result = await operation(controller.signal);
761
+ if (timedOut)
762
+ throw new Error(message);
763
+ return result;
764
+ }
765
+ catch (error) {
766
+ if (timedOut)
767
+ throw new Error(message);
768
+ throw error;
562
769
  }
563
770
  finally {
564
771
  if (timer)
565
772
  clearTimeout(timer);
566
773
  }
567
774
  }
775
+ function throwIfAborted(signal) {
776
+ if (!signal.aborted)
777
+ return;
778
+ throw signal.reason instanceof Error
779
+ ? signal.reason
780
+ : new Error("Platform delivery was aborted");
781
+ }
568
782
  function sanitizeFailureReason(reason) {
569
783
  const oneLine = reason.replace(/\s+/g, " ").trim();
570
784
  const withoutEnvNames = oneLine.replace(/\b[A-Z][A-Z0-9_]*(?:API_KEY|PRIVATE_KEY|SECRET|TOKEN)\b/g, "a required credential");