@agent-native/core 0.130.0 → 0.130.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 (108) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +12 -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/core/src/server/email-template.ts +43 -13
  13. package/corpus/core/src/sharing/actions/share-resource.ts +26 -4
  14. package/corpus/core/src/sharing/index.ts +1 -0
  15. package/corpus/core/src/sharing/registry.ts +23 -0
  16. package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +3 -1
  17. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  18. package/corpus/templates/clips/app/components/meetings/day-header.tsx +1 -1
  19. package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +1 -22
  20. package/corpus/templates/clips/app/components/workspace/branding-editor.tsx +26 -1
  21. package/corpus/templates/clips/app/components/workspace/organization-identity-card.tsx +107 -0
  22. package/corpus/templates/clips/app/i18n/en-US.ts +3 -4
  23. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +0 -10
  24. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +23 -51
  25. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +1 -8
  26. package/corpus/templates/clips/changelog/2026-07-29-clearing-all-recording-edits-no-longer-leaves-the-editor-unr.md +6 -0
  27. package/corpus/templates/clips/changelog/2026-07-29-clip-share-emails-now-open-with-who-shared-the-clip-add-a-su.md +6 -0
  28. package/corpus/templates/clips/changelog/2026-07-29-finished-meeting-pills-now-have-a-clear-close-button.md +6 -0
  29. package/corpus/templates/clips/changelog/2026-07-29-meeting-cards-no-longer-flicker-or-disappear-when-hovered-in.md +6 -0
  30. package/corpus/templates/clips/changelog/2026-07-29-organization-name-logo-brand-color-and-default-visibility-no.md +6 -0
  31. package/corpus/templates/clips/changelog/2026-07-29-transcript-lines-on-the-meeting-page-no-longer-jump-to-the-r.md +6 -0
  32. package/corpus/templates/clips/changelog/2026-07-29-when-a-meeting-wraps-up-the-recording-pill-now-shows-a-green.md +6 -0
  33. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +6 -3
  34. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +112 -32
  35. package/corpus/templates/clips/desktop/src/styles.css +42 -2
  36. package/corpus/templates/clips/server/db/index.ts +6 -0
  37. package/corpus/templates/clips/server/lib/share-email-hero.ts +33 -0
  38. package/corpus/templates/clips/shared/clips-ai-prefs.ts +0 -1
  39. package/corpus/templates/dispatch/changelog/2026-07-26-slack-replies-recover-after-connected-app-updates.md +6 -0
  40. package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +15 -0
  41. package/corpus/templates/slides/actions/delete-design-system.ts +145 -0
  42. package/corpus/templates/slides/actions/list-design-systems.ts +94 -2
  43. package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +43 -1
  44. package/corpus/templates/slides/app/hooks/use-design-systems.ts +2 -0
  45. package/corpus/templates/slides/app/i18n/en-US.ts +7 -0
  46. package/corpus/templates/slides/app/pages/DesignSystems.tsx +56 -1
  47. package/corpus/templates/slides/changelog/2026-07-28-delete-a-design-system-you-own-from-the-design-systems-page-.md +6 -0
  48. package/dist/collab/awareness.d.ts +2 -2
  49. package/dist/collab/awareness.d.ts.map +1 -1
  50. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  51. package/dist/integrations/a2a-continuation-processor.d.ts +5 -0
  52. package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
  53. package/dist/integrations/a2a-continuation-processor.js +293 -79
  54. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  55. package/dist/integrations/a2a-continuations-store.d.ts +23 -1
  56. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  57. package/dist/integrations/a2a-continuations-store.js +249 -15
  58. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  59. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  60. package/dist/integrations/adapters/slack.js +155 -31
  61. package/dist/integrations/adapters/slack.js.map +1 -1
  62. package/dist/integrations/integration-campaign-recovery.d.ts.map +1 -1
  63. package/dist/integrations/integration-campaign-recovery.js +3 -1
  64. package/dist/integrations/integration-campaign-recovery.js.map +1 -1
  65. package/dist/integrations/plugin.d.ts.map +1 -1
  66. package/dist/integrations/plugin.js +29 -12
  67. package/dist/integrations/plugin.js.map +1 -1
  68. package/dist/integrations/task-queue-stats.d.ts +12 -0
  69. package/dist/integrations/task-queue-stats.d.ts.map +1 -1
  70. package/dist/integrations/task-queue-stats.js +106 -0
  71. package/dist/integrations/task-queue-stats.js.map +1 -1
  72. package/dist/integrations/types.d.ts +24 -6
  73. package/dist/integrations/types.d.ts.map +1 -1
  74. package/dist/integrations/types.js.map +1 -1
  75. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  76. package/dist/integrations/webhook-handler.js +12 -3
  77. package/dist/integrations/webhook-handler.js.map +1 -1
  78. package/dist/notifications/routes.d.ts +3 -3
  79. package/dist/observability/routes.d.ts +3 -3
  80. package/dist/server/email-template.d.ts +4 -0
  81. package/dist/server/email-template.d.ts.map +1 -1
  82. package/dist/server/email-template.js +25 -8
  83. package/dist/server/email-template.js.map +1 -1
  84. package/dist/server/realtime-token.d.ts +1 -1
  85. package/dist/server/transcribe-voice.d.ts +1 -1
  86. package/dist/sharing/actions/share-resource.d.ts.map +1 -1
  87. package/dist/sharing/actions/share-resource.js +23 -4
  88. package/dist/sharing/actions/share-resource.js.map +1 -1
  89. package/dist/sharing/index.d.ts +1 -1
  90. package/dist/sharing/index.d.ts.map +1 -1
  91. package/dist/sharing/index.js.map +1 -1
  92. package/dist/sharing/registry.d.ts +23 -0
  93. package/dist/sharing/registry.d.ts.map +1 -1
  94. package/dist/sharing/registry.js.map +1 -1
  95. package/package.json +1 -1
  96. package/src/integrations/a2a-continuation-processor.ts +481 -122
  97. package/src/integrations/a2a-continuations-store.ts +345 -14
  98. package/src/integrations/adapters/slack.ts +243 -53
  99. package/src/integrations/integration-campaign-recovery.ts +4 -1
  100. package/src/integrations/plugin.ts +39 -21
  101. package/src/integrations/task-queue-stats.ts +146 -0
  102. package/src/integrations/types.ts +25 -4
  103. package/src/integrations/webhook-handler.ts +18 -5
  104. package/src/server/email-template.ts +43 -13
  105. package/src/sharing/actions/share-resource.ts +26 -4
  106. package/src/sharing/index.ts +1 -0
  107. package/src/sharing/registry.ts +23 -0
  108. package/corpus/templates/clips/app/routes/_app.settings.organization.tsx +0 -232
@@ -2,6 +2,7 @@ import {
2
2
  appendA2AArtifactLinks,
3
3
  extractA2AArtifactIdentities,
4
4
  stripA2APersistedArtifactMarkers,
5
+ type A2AArtifactIdentity,
5
6
  } from "../a2a/artifact-response.js";
6
7
  import { A2AClient, signA2AToken } from "../a2a/client.js";
7
8
  import type { Task } from "../a2a/types.js";
@@ -18,20 +19,28 @@ import {
18
19
  claimA2AContinuation,
19
20
  claimA2AContinuationDelivery,
20
21
  claimDueA2AContinuations,
21
- completeA2AContinuation,
22
22
  failA2AContinuationsForIntegrationTask,
23
23
  failA2AContinuation,
24
+ finalizeA2ATerminalHistory,
24
25
  getA2AContinuation,
25
- hasActiveA2AContinuationsForIntegrationTask,
26
+ getA2AContinuationTaskOutcome,
27
+ hasOnlyLegacyFailedA2AContinuationsForIntegrationTask,
28
+ hasPendingConfirmedA2ADeliveryForIntegrationTask,
26
29
  listRecoverableA2AIntegrationTasks,
27
30
  recoverDueA2AContinuationIds,
31
+ recordA2ATerminalDeliveryReceipt,
32
+ retainA2AUnconfirmedDeliveryClaim,
28
33
  rescheduleA2AContinuation,
34
+ saveA2AVerifiedArtifactCheckpoint,
29
35
  type A2AContinuation,
36
+ type A2ATerminalDeliveryKind,
37
+ type A2ATerminalHistoryPayload,
30
38
  type RecoverableA2AIntegrationTask,
31
39
  } from "./a2a-continuations-store.js";
32
40
  import {
33
41
  completeIntegrationCampaignTaskAfterA2A,
34
42
  failDisabledIntegrationCampaignTask,
43
+ failIntegrationCampaignTaskDeliveryContainment,
35
44
  getIntegrationCampaignForTask,
36
45
  } from "./integration-campaigns-store.js";
37
46
  import {
@@ -67,6 +76,23 @@ const PLATFORM_SEND_TIMEOUT_MS = 12_000;
67
76
  const DISPATCH_SETTLE_WAIT_MS = 2_000;
68
77
  const COMPLETE_AFTER_DELIVERY_ATTEMPTS = 3;
69
78
 
79
+ function logA2AContinuationTransition(
80
+ event: string,
81
+ continuation: Pick<
82
+ A2AContinuation,
83
+ "id" | "integrationTaskId" | "a2aTaskId" | "attempts" | "status"
84
+ >,
85
+ ): void {
86
+ console.info("[integrations] a2a-continuation-transition", {
87
+ event,
88
+ continuationId: continuation.id,
89
+ integrationTaskId: continuation.integrationTaskId,
90
+ downstreamTaskId: continuation.a2aTaskId,
91
+ attempt: continuation.attempts,
92
+ status: continuation.status,
93
+ });
94
+ }
95
+
70
96
  export async function dispatchA2AContinuation(
71
97
  continuationId: string,
72
98
  webhookBaseUrl?: string,
@@ -153,17 +179,129 @@ export async function processA2AContinuationById(
153
179
  await processClaimedContinuation(continuation, options);
154
180
  }
155
181
 
182
+ export async function recoverA2AContinuationAfterProcessorFailure(
183
+ continuationId: string,
184
+ options: {
185
+ adapters: Map<string, PlatformAdapter>;
186
+ reason: string;
187
+ },
188
+ ): Promise<void> {
189
+ const continuation = await getA2AContinuation(continuationId);
190
+ if (
191
+ !continuation ||
192
+ continuation.status === "completed" ||
193
+ continuation.status === "failed"
194
+ ) {
195
+ return;
196
+ }
197
+ if (
198
+ continuation.terminalDeliveryConfirmedAt != null &&
199
+ continuation.terminalHistoryPayload
200
+ ) {
201
+ await persistAndFinalizeConfirmedA2ADelivery(continuation);
202
+ return;
203
+ }
204
+ const adapter = options.adapters.get(continuation.platform);
205
+ if (continuation.attempts < MAX_ATTEMPTS) {
206
+ logA2AContinuationTransition("processor_released", continuation);
207
+ await rescheduleAndRedispatchA2AContinuation(continuation.id);
208
+ return;
209
+ }
210
+ if (!adapter) {
211
+ const reason = `Unknown platform: ${continuation.platform}`;
212
+ logA2AContinuationTransition(
213
+ "processor_exhausted_without_adapter",
214
+ continuation,
215
+ );
216
+ await failA2AContinuationsForIntegrationTask(
217
+ continuation.integrationTaskId,
218
+ reason,
219
+ );
220
+ if (
221
+ await hasPendingConfirmedA2ADeliveryForIntegrationTask(
222
+ continuation.integrationTaskId,
223
+ )
224
+ ) {
225
+ await dispatchPendingIntegrationTask({
226
+ taskId: continuation.integrationTaskId,
227
+ task: {
228
+ platform: continuation.platform,
229
+ externalThreadId: continuation.externalThreadId,
230
+ platformContext: continuation.incoming.platformContext,
231
+ },
232
+ campaignContinuation: true,
233
+ allowPortableConfirmedReceiptReconciliation: true,
234
+ }).catch((err) => {
235
+ console.error(
236
+ `[integrations] Failed to wake confirmed sibling history for ${continuation.integrationTaskId}:`,
237
+ err,
238
+ );
239
+ });
240
+ return;
241
+ }
242
+ await failIntegrationCampaignTaskDeliveryContainment(
243
+ continuation.integrationTaskId,
244
+ reason,
245
+ );
246
+ return;
247
+ }
248
+
249
+ const progress = await resumeA2AContinuationProgress(continuation, adapter);
250
+ if (continuation.verifiedArtifactCheckpoint) {
251
+ logA2AContinuationTransition(
252
+ "processor_exhausted_checkpoint_delivery",
253
+ continuation,
254
+ );
255
+ await deliverAndCompleteA2AContinuation(
256
+ continuation,
257
+ adapter,
258
+ formatRecoverableArtifactFallbackText(
259
+ continuation.verifiedArtifactCheckpoint,
260
+ ),
261
+ progress,
262
+ );
263
+ return;
264
+ }
265
+ logA2AContinuationTransition(
266
+ "processor_exhausted_failure_notice",
267
+ continuation,
268
+ );
269
+ await notifyAndFailA2AContinuation(
270
+ continuation,
271
+ adapter,
272
+ options.reason,
273
+ progress,
274
+ );
275
+ }
276
+
156
277
  export async function processDueA2AContinuations(options: {
157
278
  adapters: Map<string, PlatformAdapter>;
158
279
  limit?: number;
159
280
  }): Promise<void> {
160
281
  const continuations = await claimDueA2AContinuations(options.limit ?? 5);
161
282
  for (const continuation of continuations) {
162
- await processClaimedContinuation(continuation, options).catch((err) =>
163
- console.error(
164
- `[integrations] A2A continuation ${continuation.id} failed:`,
165
- err,
166
- ),
283
+ await processClaimedContinuation(continuation, options).catch(
284
+ async (err) => {
285
+ console.error(
286
+ `[integrations] A2A continuation ${continuation.id} failed; durable recovery requested`,
287
+ );
288
+ try {
289
+ await recoverA2AContinuationAfterProcessorFailure(continuation.id, {
290
+ adapters: options.adapters,
291
+ reason:
292
+ err instanceof Error
293
+ ? err.message.slice(0, 500)
294
+ : "continuation processing failed",
295
+ });
296
+ } catch (recoveryError) {
297
+ console.error(
298
+ `[integrations] A2A continuation ${continuation.id} recovery failed; later continuations will continue`,
299
+ recoveryError instanceof Error
300
+ ? recoveryError.name
301
+ : "recovery_error",
302
+ );
303
+ }
304
+ },
167
305
  );
168
306
  }
169
307
  }
@@ -182,6 +320,7 @@ export async function recoverDueA2AContinuations(options?: {
182
320
  const limit = options?.limit ?? 5;
183
321
  const candidateTasks = await listRecoverableA2AIntegrationTasks(200);
184
322
  const eligibleTaskIds: string[] = [];
323
+ const confirmedHistoryTaskIds: string[] = [];
185
324
  for (const task of candidateTasks) {
186
325
  const enabled = isIntegrationDurableDispatchEnabledForTask({
187
326
  platform: task.platform,
@@ -192,12 +331,24 @@ export async function recoverDueA2AContinuations(options?: {
192
331
  });
193
332
  if (enabled) {
194
333
  eligibleTaskIds.push(task.id);
195
- if (eligibleTaskIds.length >= limit) break;
334
+ } else if (task.hasPendingConfirmedDelivery) {
335
+ confirmedHistoryTaskIds.push(task.id);
196
336
  } else {
197
337
  await failDisabledDurableA2ATask(task);
198
338
  }
339
+ if (eligibleTaskIds.length + confirmedHistoryTaskIds.length >= limit) break;
199
340
  }
200
341
  const ids = await recoverDueA2AContinuationIds(limit, eligibleTaskIds);
342
+ const remaining = Math.max(0, limit - ids.length);
343
+ const confirmedHistoryIds =
344
+ remaining > 0 && confirmedHistoryTaskIds.length > 0
345
+ ? await recoverDueA2AContinuationIds(
346
+ remaining,
347
+ confirmedHistoryTaskIds,
348
+ true,
349
+ )
350
+ : [];
351
+ ids.push(...confirmedHistoryIds);
201
352
  let dispatched = 0;
202
353
  let failed = 0;
203
354
 
@@ -223,6 +374,13 @@ async function processClaimedContinuation(
223
374
  continuation: A2AContinuation,
224
375
  options: { adapters: Map<string, PlatformAdapter> },
225
376
  ): Promise<void> {
377
+ if (
378
+ continuation.terminalDeliveryConfirmedAt != null &&
379
+ continuation.terminalHistoryPayload
380
+ ) {
381
+ await persistAndFinalizeConfirmedA2ADelivery(continuation);
382
+ return;
383
+ }
226
384
  if (!(await durableContinuationScopeStillEnabled(continuation))) return;
227
385
  const adapter = options.adapters.get(continuation.platform);
228
386
  if (!adapter) {
@@ -244,7 +402,7 @@ async function processClaimedContinuation(
244
402
  const recoverableArtifactSecrets =
245
403
  await resolveContinuationArtifactSecrets(continuation);
246
404
  let task: Task | null = null;
247
- let latestRecoverableArtifactText: string | null = null;
405
+ let latestRecoverableArtifactText = continuation.verifiedArtifactCheckpoint;
248
406
 
249
407
  try {
250
408
  while (Date.now() < deadline) {
@@ -255,7 +413,13 @@ async function processClaimedContinuation(
255
413
  recoverableArtifactSecrets,
256
414
  );
257
415
  if (recoverableArtifactText) {
258
- latestRecoverableArtifactText = recoverableArtifactText;
416
+ latestRecoverableArtifactText = await saveA2AVerifiedArtifactCheckpoint(
417
+ continuation.id,
418
+ recoverableArtifactText,
419
+ );
420
+ if (latestRecoverableArtifactText) {
421
+ logA2AContinuationTransition("checkpoint_persisted", continuation);
422
+ }
259
423
  }
260
424
  if (TERMINAL_STATES.has(task.status.state)) break;
261
425
  await reportA2AContinuationProgress(continuation, progress, task);
@@ -287,6 +451,15 @@ async function processClaimedContinuation(
287
451
  return;
288
452
  }
289
453
  if (continuation.attempts >= MAX_ATTEMPTS) {
454
+ if (latestRecoverableArtifactText) {
455
+ await deliverAndCompleteA2AContinuation(
456
+ continuation,
457
+ adapter,
458
+ formatRecoverableArtifactFallbackText(latestRecoverableArtifactText),
459
+ progress,
460
+ );
461
+ return;
462
+ }
290
463
  await notifyAndFailA2AContinuation(
291
464
  continuation,
292
465
  adapter,
@@ -396,6 +569,19 @@ async function durableContinuationScopeStillEnabled(
396
569
  });
397
570
  if (enabled) return true;
398
571
 
572
+ if (
573
+ await hasPendingConfirmedA2ADeliveryForIntegrationTask(
574
+ continuation.integrationTaskId,
575
+ )
576
+ ) {
577
+ await failA2AContinuation(
578
+ continuation.id,
579
+ "Durable integration campaign was disabled before this continuation delivered",
580
+ );
581
+ await reconcileTerminalA2AParentIfDisabled(continuation.integrationTaskId);
582
+ return false;
583
+ }
584
+
399
585
  await failDisabledDurableA2ATask({
400
586
  id: continuation.integrationTaskId,
401
587
  platform: task?.platform ?? continuation.platform,
@@ -407,7 +593,10 @@ async function durableContinuationScopeStillEnabled(
407
593
  }
408
594
 
409
595
  async function failDisabledDurableA2ATask(
410
- task: RecoverableA2AIntegrationTask,
596
+ task: Pick<
597
+ RecoverableA2AIntegrationTask,
598
+ "id" | "platform" | "externalThreadId" | "dispatchScope" | "status"
599
+ >,
411
600
  ): Promise<void> {
412
601
  const message = "Durable integration campaign was disabled for this scope";
413
602
  await failA2AContinuationsForIntegrationTask(task.id, message);
@@ -430,6 +619,49 @@ async function failDisabledDurableA2ATask(
430
619
  }
431
620
  }
432
621
 
622
+ export async function reconcileTerminalA2AParentIfDisabled(
623
+ integrationTaskId: string,
624
+ ): Promise<boolean> {
625
+ if (
626
+ (await getA2AContinuationTaskOutcome(integrationTaskId)) !==
627
+ "terminal-without-delivery"
628
+ ) {
629
+ return false;
630
+ }
631
+ if (
632
+ await hasOnlyLegacyFailedA2AContinuationsForIntegrationTask(
633
+ integrationTaskId,
634
+ )
635
+ ) {
636
+ await failIntegrationCampaignTaskDeliveryContainment(
637
+ integrationTaskId,
638
+ "Legacy A2A continuation ended without durable delivery proof",
639
+ );
640
+ return true;
641
+ }
642
+ const task = await getPendingTask(integrationTaskId);
643
+ if (
644
+ !task ||
645
+ isIntegrationDurableDispatchEnabledForTask({
646
+ platform: task.platform,
647
+ externalThreadId: task.externalThreadId,
648
+ platformContext: task.dispatchScope
649
+ ? { channelId: task.dispatchScope }
650
+ : undefined,
651
+ })
652
+ ) {
653
+ return false;
654
+ }
655
+ await failDisabledDurableA2ATask({
656
+ id: task.id,
657
+ platform: task.platform,
658
+ externalThreadId: task.externalThreadId,
659
+ dispatchScope: task.dispatchScope,
660
+ status: task.status,
661
+ });
662
+ return true;
663
+ }
664
+
433
665
  async function resumeA2AContinuationProgress(
434
666
  continuation: A2AContinuation,
435
667
  adapter: PlatformAdapter,
@@ -507,21 +739,29 @@ async function notifyAndFailA2AContinuation(
507
739
  continuation.id,
508
740
  );
509
741
  if (!deliveryContinuation) return;
742
+ logA2AContinuationTransition(
743
+ "failure_delivery_claimed",
744
+ deliveryContinuation,
745
+ );
510
746
 
511
747
  const message = formatContinuationFailureMessage(
512
748
  deliveryContinuation,
513
749
  reason,
514
750
  );
751
+ let outgoing: OutgoingMessage;
752
+ let deliveryReceipt: PlatformDeliveryReceipt;
515
753
  try {
516
- const outgoing = adapter.formatAgentResponse(message);
517
- await withTimeout(
518
- deliverA2AContinuationResponse(
519
- adapter,
520
- deliveryContinuation,
521
- outgoing,
522
- progress,
523
- "error",
524
- ),
754
+ outgoing = adapter.formatAgentResponse(message);
755
+ deliveryReceipt = await withTimeout(
756
+ (signal) =>
757
+ deliverA2AContinuationResponse(
758
+ adapter,
759
+ deliveryContinuation,
760
+ outgoing,
761
+ progress,
762
+ "error",
763
+ signal,
764
+ ),
525
765
  PLATFORM_SEND_TIMEOUT_MS,
526
766
  `${deliveryContinuation.platform} failure notification timed out`,
527
767
  );
@@ -530,17 +770,20 @@ async function notifyAndFailA2AContinuation(
530
770
  `[integrations] Failed to notify ${deliveryContinuation.platform} about failed A2A continuation ${deliveryContinuation.id}:`,
531
771
  err,
532
772
  );
533
- if (deliveryContinuation.attempts >= MAX_ATTEMPTS) {
534
- await failA2AContinuation(deliveryContinuation.id, reason);
535
- await completeParentCampaignAfterTerminalA2A(deliveryContinuation);
536
- return;
537
- }
538
773
  await rescheduleAndRedispatchA2AContinuation(deliveryContinuation.id);
539
774
  return;
540
775
  }
541
776
 
542
- await failA2AContinuation(deliveryContinuation.id, reason);
543
- await completeParentCampaignAfterTerminalA2A(deliveryContinuation);
777
+ const confirmed = await recordTerminalA2ADelivery(
778
+ deliveryContinuation,
779
+ "failure",
780
+ outgoing,
781
+ deliveryReceipt,
782
+ [],
783
+ reason,
784
+ );
785
+ if (!confirmed) return;
786
+ await persistAndFinalizeConfirmedA2ADelivery(confirmed);
544
787
  }
545
788
 
546
789
  async function deliverAndCompleteA2AContinuation(
@@ -554,59 +797,49 @@ async function deliverAndCompleteA2AContinuation(
554
797
  continuation.id,
555
798
  );
556
799
  if (!deliveryContinuation) return;
800
+ logA2AContinuationTransition(
801
+ "response_delivery_claimed",
802
+ deliveryContinuation,
803
+ );
557
804
 
805
+ let outgoing: OutgoingMessage;
806
+ let deliveryReceipt: PlatformDeliveryReceipt;
807
+ const artifactSecrets =
808
+ await resolveContinuationArtifactSecrets(deliveryContinuation);
809
+ const artifacts = extractA2AArtifactIdentities(
810
+ [{ tool: "call-agent", result: text }],
811
+ { persistedArtifactSecrets: artifactSecrets },
812
+ );
558
813
  try {
559
- const outgoing = adapter.formatAgentResponse(
814
+ outgoing = adapter.formatAgentResponse(
560
815
  stripA2APersistedArtifactMarkers(text),
561
816
  );
562
- const deliveryReceipt = await withTimeout(
563
- deliverA2AContinuationResponse(
564
- adapter,
565
- deliveryContinuation,
566
- outgoing,
567
- progress,
568
- "done",
569
- ),
817
+ deliveryReceipt = await withTimeout(
818
+ (signal) =>
819
+ deliverA2AContinuationResponse(
820
+ adapter,
821
+ deliveryContinuation,
822
+ outgoing,
823
+ progress,
824
+ "done",
825
+ signal,
826
+ ),
570
827
  PLATFORM_SEND_TIMEOUT_MS,
571
828
  `${deliveryContinuation.platform} response delivery timed out`,
572
829
  );
573
- let persistenceError: unknown;
574
- const artifactSecrets =
575
- await resolveContinuationArtifactSecrets(deliveryContinuation);
576
- for (let attempt = 0; attempt < 3; attempt += 1) {
577
- try {
578
- await persistA2AContinuationDelivery(
579
- deliveryContinuation,
580
- outgoing,
581
- deliveryReceipt,
582
- text,
583
- artifactSecrets,
584
- );
585
- persistenceError = undefined;
586
- break;
587
- } catch (err) {
588
- persistenceError = err;
589
- }
590
- }
591
- if (persistenceError) {
592
- console.error(
593
- `[integrations] Delivered A2A continuation ${deliveryContinuation.id} but could not persist its thread history:`,
594
- persistenceError,
595
- );
596
- }
597
- } catch (err) {
598
- if (deliveryContinuation.attempts >= MAX_ATTEMPTS) {
599
- await failA2AContinuation(
600
- deliveryContinuation.id,
601
- err instanceof Error ? err.message : String(err),
602
- );
603
- return;
604
- }
830
+ const confirmed = await recordTerminalA2ADelivery(
831
+ deliveryContinuation,
832
+ "success",
833
+ outgoing,
834
+ deliveryReceipt,
835
+ artifacts,
836
+ );
837
+ if (!confirmed) return;
838
+ await persistAndFinalizeConfirmedA2ADelivery(confirmed);
839
+ } catch {
605
840
  await rescheduleAndRedispatchA2AContinuation(deliveryContinuation.id);
606
841
  return;
607
842
  }
608
-
609
- await completeAfterSuccessfulDelivery(deliveryContinuation);
610
843
  }
611
844
 
612
845
  async function deliverA2AContinuationResponse(
@@ -615,18 +848,24 @@ async function deliverA2AContinuationResponse(
615
848
  message: OutgoingMessage,
616
849
  progress: PlatformRunProgress | null,
617
850
  status: "done" | "error",
851
+ signal: AbortSignal,
618
852
  ): Promise<PlatformDeliveryReceipt> {
619
853
  if (progress) {
620
854
  try {
621
- await progress.onEvent({
622
- type: "agent_call",
623
- agent: continuation.agentName,
624
- status,
625
- });
626
- const receipt = await progress.complete(message);
855
+ await progress.onEvent(
856
+ {
857
+ type: "agent_call",
858
+ agent: continuation.agentName,
859
+ status,
860
+ },
861
+ { signal },
862
+ );
863
+ throwIfAborted(signal);
864
+ const receipt = await progress.complete(message, { signal });
627
865
  if (receipt?.status === "delivered") return receipt;
628
866
  throw new Error("Continuation progress completed without delivery proof");
629
867
  } catch {
868
+ throwIfAborted(signal);
630
869
  // A resumed Slack stream can no longer be finalized (for example when
631
870
  // chat.stopStream rejects). Preserve the final answer with the same
632
871
  // thread reply fallback used by the initial webhook run. Also ask the
@@ -635,14 +874,22 @@ async function deliverA2AContinuationResponse(
635
874
  try {
636
875
  await progress.fail?.(
637
876
  "I couldn't update the live response, but I posted the final result in this thread.",
877
+ { signal },
638
878
  );
639
879
  } catch {
640
880
  // The thread reply below is still the authoritative final answer.
641
881
  }
882
+ logA2AContinuationTransition("native_progress_fallback", continuation);
642
883
  }
643
884
  }
885
+ throwIfAborted(signal);
644
886
  const receipt = await adapter.sendResponse(message, continuation.incoming, {
645
- placeholderRef: continuation.placeholderRef ?? undefined,
887
+ idempotencyKey: `a2a-continuation:${continuation.id}`,
888
+ reconcileAfter: continuation.createdAt,
889
+ signal,
890
+ placeholderRef:
891
+ progress?.responseTargetRef ?? continuation.placeholderRef ?? undefined,
892
+ strictTargetRef: true,
646
893
  });
647
894
  if (receipt?.status !== "delivered") {
648
895
  throw new Error("Continuation response completed without delivery proof");
@@ -652,18 +899,19 @@ async function deliverA2AContinuationResponse(
652
899
 
653
900
  async function persistA2AContinuationDelivery(
654
901
  continuation: A2AContinuation,
655
- outgoing: OutgoingMessage,
656
- receipt: PlatformDeliveryReceipt,
657
- artifactText: string,
658
- artifactSecrets: readonly string[],
902
+ history: A2ATerminalHistoryPayload,
659
903
  ): Promise<void> {
660
904
  const mapping = await getThreadMapping(
661
905
  continuation.platform,
662
906
  continuation.externalThreadId,
663
907
  );
664
- if (!mapping) return;
908
+ if (!mapping) {
909
+ throw new Error("Integration thread mapping is not available");
910
+ }
665
911
  const thread = await getThread(mapping.internalThreadId);
666
- if (!thread) return;
912
+ if (!thread) {
913
+ throw new Error("Integration chat thread is not available");
914
+ }
667
915
 
668
916
  let repo: any;
669
917
  try {
@@ -673,33 +921,32 @@ async function persistA2AContinuationDelivery(
673
921
  }
674
922
  if (!Array.isArray(repo.messages)) repo.messages = [];
675
923
 
676
- const artifacts = extractA2AArtifactIdentities(
677
- [{ tool: "call-agent", result: artifactText }],
678
- {
679
- persistedArtifactSecrets: artifactSecrets,
680
- },
681
- );
682
924
  const metadata: Record<string, unknown> = {
683
925
  integrationDeliveryAttempted: true,
684
926
  integrationDelivery: {
685
927
  platform: continuation.platform,
686
928
  status: "delivered",
687
- text: outgoing.text,
688
- deliveredAt: new Date().toISOString(),
689
- ...(receipt.messageRefs?.length
690
- ? { messageRefs: receipt.messageRefs }
929
+ text: history.text,
930
+ deliveredAt: history.deliveredAt,
931
+ ...(history.messageRefs.length
932
+ ? { messageRefs: history.messageRefs }
691
933
  : {}),
692
934
  },
693
935
  };
694
- if (artifacts.length > 0) metadata.integrationArtifacts = artifacts;
695
-
696
- repo.messages.push({
697
- id: `msg-${Date.now()}-assistant-continuation`,
698
- role: "assistant",
699
- content: [{ type: "text", text: outgoing.text }],
700
- createdAt: new Date().toISOString(),
701
- metadata,
702
- });
936
+ if (history.artifacts.length > 0) {
937
+ metadata.integrationArtifacts = history.artifacts;
938
+ }
939
+
940
+ const messageId = `msg-${continuation.id}-assistant-continuation`;
941
+ if (!repo.messages.some((message: any) => message?.id === messageId)) {
942
+ repo.messages.push({
943
+ id: messageId,
944
+ role: "assistant",
945
+ content: [{ type: "text", text: history.text }],
946
+ createdAt: history.deliveredAt,
947
+ metadata,
948
+ });
949
+ }
703
950
  const meta = extractThreadMeta(repo);
704
951
  await updateThreadData(
705
952
  mapping.internalThreadId,
@@ -722,25 +969,110 @@ async function rescheduleAndRedispatchA2AContinuation(
722
969
  });
723
970
  }
724
971
 
725
- async function completeAfterSuccessfulDelivery(
972
+ async function recordTerminalA2ADelivery(
973
+ continuation: A2AContinuation,
974
+ kind: A2ATerminalDeliveryKind,
975
+ outgoing: OutgoingMessage,
976
+ receipt: PlatformDeliveryReceipt,
977
+ artifacts: A2AArtifactIdentity[],
978
+ errorMessage?: string,
979
+ ): Promise<A2AContinuation | null> {
980
+ const historyPayload: A2ATerminalHistoryPayload = {
981
+ text: outgoing.text,
982
+ deliveredAt: new Date().toISOString(),
983
+ messageRefs: receipt.messageRefs ?? [],
984
+ artifacts,
985
+ };
986
+ for (let attempt = 0; attempt < COMPLETE_AFTER_DELIVERY_ATTEMPTS; attempt++) {
987
+ try {
988
+ const confirmed = await recordA2ATerminalDeliveryReceipt(
989
+ continuation.id,
990
+ kind,
991
+ historyPayload,
992
+ errorMessage,
993
+ );
994
+ logA2AContinuationTransition(
995
+ kind === "success"
996
+ ? "response_delivery_confirmed"
997
+ : "failure_delivery_confirmed",
998
+ continuation,
999
+ );
1000
+ return confirmed;
1001
+ } catch {}
1002
+ }
1003
+
1004
+ console.error(
1005
+ `[integrations] ${continuation.platform} accepted terminal A2A delivery for ${continuation.id}, ` +
1006
+ `but recording its receipt failed after ${COMPLETE_AFTER_DELIVERY_ATTEMPTS} attempts. Leaving it in delivering for stale recovery.`,
1007
+ );
1008
+ try {
1009
+ await retainA2AUnconfirmedDeliveryClaim(continuation.id);
1010
+ } catch (err) {
1011
+ console.error(
1012
+ `[integrations] Failed to retain unconfirmed A2A delivery claim ${continuation.id}:`,
1013
+ err instanceof Error ? err.name : "receipt_recovery_error",
1014
+ );
1015
+ }
1016
+ return null;
1017
+ }
1018
+
1019
+ async function persistAndFinalizeConfirmedA2ADelivery(
726
1020
  continuation: A2AContinuation,
727
1021
  ): Promise<void> {
1022
+ const history = continuation.terminalHistoryPayload;
1023
+ if (!history || continuation.terminalDeliveryConfirmedAt == null) {
1024
+ throw new Error("Confirmed A2A delivery is missing durable history");
1025
+ }
728
1026
  let lastError: unknown;
729
- for (let attempt = 0; attempt < COMPLETE_AFTER_DELIVERY_ATTEMPTS; attempt++) {
1027
+ let historyFinalized = false;
1028
+ for (let attempt = 0; attempt < 3; attempt += 1) {
730
1029
  try {
731
- await completeA2AContinuation(continuation.id);
732
- await completeParentCampaignAfterTerminalA2A(continuation);
1030
+ if (!historyFinalized) {
1031
+ await persistA2AContinuationDelivery(continuation, history);
1032
+ await finalizeA2ATerminalHistory(continuation.id);
1033
+ historyFinalized = true;
1034
+ logA2AContinuationTransition(
1035
+ "terminal_history_persisted",
1036
+ continuation,
1037
+ );
1038
+ }
1039
+ await completeParentCampaignAfterTerminalA2A({
1040
+ ...continuation,
1041
+ status:
1042
+ continuation.terminalDeliveryKind === "success"
1043
+ ? "completed"
1044
+ : "failed",
1045
+ });
733
1046
  return;
734
1047
  } catch (err) {
735
1048
  lastError = err;
736
1049
  }
737
1050
  }
738
-
739
1051
  console.error(
740
- `[integrations] ${continuation.platform} accepted A2A continuation ${continuation.id}, ` +
741
- "but marking it completed failed. Leaving it in delivering for stale-delivery recovery.",
742
- lastError,
1052
+ historyFinalized
1053
+ ? `[integrations] A2A continuation ${continuation.id} finalized history but parent completion remains retryable:`
1054
+ : `[integrations] A2A continuation ${continuation.id} has a provider receipt but its history remains retryable:`,
1055
+ lastError instanceof Error ? lastError.name : "persistence_error",
743
1056
  );
1057
+ if (historyFinalized) {
1058
+ await dispatchPendingIntegrationTask({
1059
+ taskId: continuation.integrationTaskId,
1060
+ task: {
1061
+ platform: continuation.platform,
1062
+ externalThreadId: continuation.externalThreadId,
1063
+ platformContext: continuation.incoming.platformContext,
1064
+ },
1065
+ campaignContinuation: true,
1066
+ allowPortableConfirmedReceiptReconciliation: true,
1067
+ }).catch((err) => {
1068
+ console.error(
1069
+ `[integrations] Failed to wake A2A parent ${continuation.integrationTaskId} after terminal history finalization:`,
1070
+ err,
1071
+ );
1072
+ });
1073
+ return;
1074
+ }
1075
+ await rescheduleAndRedispatchA2AContinuation(continuation.id);
744
1076
  }
745
1077
 
746
1078
  async function completeParentCampaignAfterTerminalA2A(
@@ -750,17 +1082,31 @@ async function completeParentCampaignAfterTerminalA2A(
750
1082
  continuation.integrationTaskId,
751
1083
  );
752
1084
  if (!campaign) return;
753
- if (
754
- await hasActiveA2AContinuationsForIntegrationTask(
755
- continuation.integrationTaskId,
756
- )
757
- ) {
1085
+ const outcome = await getA2AContinuationTaskOutcome(
1086
+ continuation.integrationTaskId,
1087
+ );
1088
+ if (outcome !== "terminal-delivered") {
1089
+ if (outcome === "terminal-without-delivery") {
1090
+ if (
1091
+ await reconcileTerminalA2AParentIfDisabled(
1092
+ continuation.integrationTaskId,
1093
+ )
1094
+ ) {
1095
+ return;
1096
+ }
1097
+ }
1098
+ if (outcome !== "active") {
1099
+ console.warn(
1100
+ `[integrations] Refusing to complete A2A parent ${continuation.integrationTaskId} with outcome ${outcome}`,
1101
+ );
1102
+ }
758
1103
  return;
759
1104
  }
760
1105
  const completed = await completeIntegrationCampaignTaskAfterA2A(
761
1106
  continuation.integrationTaskId,
762
1107
  );
763
1108
  if (!completed) return;
1109
+ logA2AContinuationTransition("parent_completed", continuation);
764
1110
 
765
1111
  const nextTask = await getNextPendingTaskForThread(
766
1112
  continuation.platform,
@@ -861,23 +1207,36 @@ function sleep(ms: number): Promise<void> {
861
1207
  }
862
1208
 
863
1209
  async function withTimeout<T>(
864
- promise: Promise<T>,
1210
+ operation: (signal: AbortSignal) => Promise<T>,
865
1211
  timeoutMs: number,
866
1212
  message: string,
867
1213
  ): Promise<T> {
1214
+ const controller = new AbortController();
868
1215
  let timer: ReturnType<typeof setTimeout> | undefined;
1216
+ let timedOut = false;
869
1217
  try {
870
- return await Promise.race([
871
- promise,
872
- new Promise<never>((_, reject) => {
873
- timer = setTimeout(() => reject(new Error(message)), timeoutMs);
874
- }),
875
- ]);
1218
+ timer = setTimeout(() => {
1219
+ timedOut = true;
1220
+ controller.abort(new Error(message));
1221
+ }, timeoutMs);
1222
+ const result = await operation(controller.signal);
1223
+ if (timedOut) throw new Error(message);
1224
+ return result;
1225
+ } catch (error) {
1226
+ if (timedOut) throw new Error(message);
1227
+ throw error;
876
1228
  } finally {
877
1229
  if (timer) clearTimeout(timer);
878
1230
  }
879
1231
  }
880
1232
 
1233
+ function throwIfAborted(signal: AbortSignal): void {
1234
+ if (!signal.aborted) return;
1235
+ throw signal.reason instanceof Error
1236
+ ? signal.reason
1237
+ : new Error("Platform delivery was aborted");
1238
+ }
1239
+
881
1240
  function sanitizeFailureReason(reason: string): string {
882
1241
  const oneLine = reason.replace(/\s+/g, " ").trim();
883
1242
  const withoutEnvNames = oneLine.replace(