@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
@@ -1,3 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+
1
3
  import type { H3Event } from "h3";
2
4
  import { createError, getHeader, readRawBody } from "h3";
3
5
 
@@ -24,6 +26,7 @@ import type {
24
26
  PlatformRunProgress,
25
27
  PlatformRunProgressRef,
26
28
  PlatformDeliveryReceipt,
29
+ PlatformDeliveryOptions,
27
30
  IntegrationContextMessage,
28
31
  IntegrationFileReference,
29
32
  } from "../types.js";
@@ -47,6 +50,9 @@ const SLACK_IDENTITY_NEGATIVE_CACHE_TTL_MS = 30 * 1_000;
47
50
  const SLACK_IDENTITY_CACHE_MAX_ENTRIES = 1_000;
48
51
  const SLACK_TOKEN_IDENTITY_CACHE_TTL_MS = 10 * 60 * 1_000;
49
52
  const SLACK_TOKEN_IDENTITY_NEGATIVE_CACHE_TTL_MS = 30 * 1_000;
53
+ const SLACK_DELIVERY_RECONCILIATION_PAGE_LIMIT = 200;
54
+ const SLACK_DELIVERY_RECONCILIATION_MAX_PAGES = 3;
55
+ const SLACK_DELIVERY_MARKER_PREFIX = "agent_native_terminal_";
50
56
 
51
57
  type SlackTokenIdentity = {
52
58
  teamId: string | null;
@@ -441,7 +447,7 @@ export function slackAdapter(
441
447
  async sendResponse(
442
448
  message: OutgoingMessage,
443
449
  context: IncomingMessage,
444
- opts?: { placeholderRef?: string },
450
+ opts?: PlatformDeliveryOptions,
445
451
  ): Promise<void | PlatformDeliveryReceipt> {
446
452
  const token = await resolveBotToken(context);
447
453
  if (!token) {
@@ -471,6 +477,22 @@ export function slackAdapter(
471
477
  }
472
478
  const restChunks = chunks.slice(1);
473
479
  const messageRefs: string[] = [];
480
+ const freshChunkIndexes = [
481
+ ...(!placeholderRef ? [0] : []),
482
+ ...restChunks.map((_, index) => index + 1),
483
+ ];
484
+ const reconciledRefs =
485
+ opts?.idempotencyKey && freshChunkIndexes.length > 0
486
+ ? await reconcileSlackDeliveryChunks(
487
+ token,
488
+ channelId,
489
+ threadTs,
490
+ opts.idempotencyKey,
491
+ freshChunkIndexes,
492
+ opts.reconcileAfter,
493
+ opts.signal,
494
+ )
495
+ : new Map<number, string>();
474
496
 
475
497
  const finalBlocks =
476
498
  blocks ??
@@ -491,40 +513,57 @@ export function slackAdapter(
491
513
  try {
492
514
  if (placeholderRef) {
493
515
  // Replace the "thinking…" placeholder in place.
494
- const res = await slackApiFetch("https://slack.com/api/chat.update", {
495
- method: "POST",
496
- headers: {
497
- Authorization: `Bearer ${token}`,
498
- "Content-Type": "application/json",
516
+ const data = (await slackApiJson(
517
+ "https://slack.com/api/chat.update",
518
+ {
519
+ method: "POST",
520
+ headers: {
521
+ Authorization: `Bearer ${token}`,
522
+ "Content-Type": "application/json",
523
+ },
524
+ body: JSON.stringify({ ...baseBody, ts: placeholderRef }),
525
+ signal: opts?.signal,
499
526
  },
500
- body: JSON.stringify({ ...baseBody, ts: placeholderRef }),
501
- });
502
- const data = (await res.json()) as {
527
+ )) as {
503
528
  ok: boolean;
504
529
  error?: string;
505
530
  ts?: string;
506
531
  };
507
532
  if (!data.ok) {
508
533
  console.error("[slack] chat.update error:", data.error);
534
+ if (opts?.strictTargetRef) {
535
+ throw new Error(data.error || "chat.update failed");
536
+ }
509
537
  // Fall back to a fresh post so the user still sees a reply
510
538
  const postedTs = await postFresh(
511
539
  token,
512
540
  channelId,
513
541
  threadTs,
514
- baseBody,
542
+ opts?.idempotencyKey
543
+ ? withSlackDeliveryMarker(baseBody, opts.idempotencyKey, 0)
544
+ : baseBody,
545
+ opts?.signal,
515
546
  );
516
547
  if (postedTs) messageRefs.push(postedTs);
517
548
  } else {
518
549
  messageRefs.push(data.ts || placeholderRef);
519
550
  }
520
551
  } else {
521
- const postedTs = await postFresh(
522
- token,
523
- channelId,
524
- threadTs,
525
- baseBody,
526
- );
527
- if (postedTs) messageRefs.push(postedTs);
552
+ const reconciledRef = reconciledRefs.get(0);
553
+ if (reconciledRef) {
554
+ messageRefs.push(reconciledRef);
555
+ } else {
556
+ const postedTs = await postFresh(
557
+ token,
558
+ channelId,
559
+ threadTs,
560
+ opts?.idempotencyKey
561
+ ? withSlackDeliveryMarker(baseBody, opts.idempotencyKey, 0)
562
+ : baseBody,
563
+ opts?.signal,
564
+ );
565
+ if (postedTs) messageRefs.push(postedTs);
566
+ }
528
567
  }
529
568
 
530
569
  // Clear the AI-assistant "is thinking…" status now that we've
@@ -534,14 +573,33 @@ export function slackAdapter(
534
573
  }
535
574
 
536
575
  // Overflow chunks (rare) — post as plain follow-ups in the same thread
537
- for (const chunk of restChunks) {
538
- const postedTs = await postFresh(token, channelId, threadTs, {
576
+ for (const [index, chunk] of restChunks.entries()) {
577
+ const chunkIndex = index + 1;
578
+ const reconciledRef = reconciledRefs.get(chunkIndex);
579
+ if (reconciledRef) {
580
+ messageRefs.push(reconciledRef);
581
+ continue;
582
+ }
583
+ const overflowBody: Record<string, unknown> = {
539
584
  channel: channelId,
540
585
  text: chunk,
541
586
  unfurl_links: false,
542
587
  unfurl_media: false,
543
588
  mrkdwn: true,
544
- });
589
+ };
590
+ const postedTs = await postFresh(
591
+ token,
592
+ channelId,
593
+ threadTs,
594
+ opts?.idempotencyKey
595
+ ? withSlackDeliveryMarker(
596
+ overflowBody,
597
+ opts.idempotencyKey,
598
+ chunkIndex,
599
+ )
600
+ : overflowBody,
601
+ opts?.signal,
602
+ );
545
603
  if (postedTs) messageRefs.push(postedTs);
546
604
  }
547
605
  return {
@@ -628,7 +686,7 @@ export function slackAdapter(
628
686
  if (target.threadRef) body.thread_ts = target.threadRef;
629
687
 
630
688
  try {
631
- const res = await slackApiFetch(
689
+ const data = (await slackApiJson(
632
690
  "https://slack.com/api/chat.postMessage",
633
691
  {
634
692
  method: "POST",
@@ -638,8 +696,7 @@ export function slackAdapter(
638
696
  },
639
697
  body: JSON.stringify(body),
640
698
  },
641
- );
642
- const data = (await res.json()) as { ok: boolean; error?: string };
699
+ )) as { ok: boolean; error?: string };
643
700
  if (!data.ok) {
644
701
  throw new Error(data.error || "chat.postMessage failed");
645
702
  }
@@ -1037,7 +1094,7 @@ function setSlackAssistantStatus(
1037
1094
  threadTs: string,
1038
1095
  status: string,
1039
1096
  ): void {
1040
- slackApiFetch("https://slack.com/api/assistant.threads.setStatus", {
1097
+ slackApiJson("https://slack.com/api/assistant.threads.setStatus", {
1041
1098
  method: "POST",
1042
1099
  headers: {
1043
1100
  Authorization: `Bearer ${token}`,
@@ -1094,6 +1151,7 @@ async function postFresh(
1094
1151
  channelId: string,
1095
1152
  threadTs: string | undefined,
1096
1153
  body: Record<string, unknown>,
1154
+ signal?: AbortSignal,
1097
1155
  ): Promise<string | undefined> {
1098
1156
  const hasBlocks =
1099
1157
  Array.isArray(body.blocks) && (body.blocks as unknown[]).length > 0;
@@ -1110,15 +1168,15 @@ async function postFresh(
1110
1168
  channel: channelId,
1111
1169
  };
1112
1170
  if (threadTs && !payload.thread_ts) payload.thread_ts = threadTs;
1113
- const res = await slackApiFetch("https://slack.com/api/chat.postMessage", {
1171
+ const data = (await slackApiJson("https://slack.com/api/chat.postMessage", {
1114
1172
  method: "POST",
1115
1173
  headers: {
1116
1174
  Authorization: `Bearer ${token}`,
1117
1175
  "Content-Type": "application/json",
1118
1176
  },
1119
1177
  body: JSON.stringify(payload),
1120
- });
1121
- const data = (await res.json()) as {
1178
+ signal,
1179
+ })) as {
1122
1180
  ok: boolean;
1123
1181
  error?: string;
1124
1182
  ts?: string;
@@ -1130,23 +1188,145 @@ async function postFresh(
1130
1188
  return data.ts;
1131
1189
  }
1132
1190
 
1133
- async function slackApiFetch(
1191
+ function slackDeliveryMarker(key: string, chunkIndex: number): string {
1192
+ const digest = createHash("sha256")
1193
+ .update(`${key}:${chunkIndex}`)
1194
+ .digest("hex")
1195
+ .slice(0, 32);
1196
+ return `${SLACK_DELIVERY_MARKER_PREFIX}${digest}`;
1197
+ }
1198
+
1199
+ function withSlackDeliveryMarker(
1200
+ body: Record<string, unknown>,
1201
+ key: string,
1202
+ chunkIndex: number,
1203
+ ): Record<string, unknown> {
1204
+ const marker = slackDeliveryMarker(key, chunkIndex);
1205
+ const blocks = Array.isArray(body.blocks) ? body.blocks : [];
1206
+ if (blocks.length > 0) {
1207
+ const [first, ...rest] = blocks;
1208
+ if (first && typeof first === "object" && !Array.isArray(first)) {
1209
+ return {
1210
+ ...body,
1211
+ blocks: [
1212
+ { ...(first as Record<string, unknown>), block_id: marker },
1213
+ ...rest,
1214
+ ],
1215
+ };
1216
+ }
1217
+ }
1218
+ return {
1219
+ ...body,
1220
+ blocks: [
1221
+ {
1222
+ type: "section",
1223
+ block_id: marker,
1224
+ text: { type: "mrkdwn", text: String(body.text ?? "") },
1225
+ },
1226
+ ],
1227
+ };
1228
+ }
1229
+
1230
+ async function reconcileSlackDeliveryChunks(
1231
+ token: string,
1232
+ channelId: string,
1233
+ threadTs: string | undefined,
1234
+ key: string,
1235
+ chunkIndexes: number[],
1236
+ reconcileAfter?: number,
1237
+ signal?: AbortSignal,
1238
+ ): Promise<Map<number, string>> {
1239
+ if (!threadTs) {
1240
+ throw new Error(
1241
+ "Cannot reconcile an idempotent Slack delivery without a thread timestamp",
1242
+ );
1243
+ }
1244
+ const refs = new Map<number, string>();
1245
+ const expectedMarkers = new Map(
1246
+ chunkIndexes.map((chunkIndex) => [
1247
+ slackDeliveryMarker(key, chunkIndex),
1248
+ chunkIndex,
1249
+ ]),
1250
+ );
1251
+ let cursor = "";
1252
+ for (
1253
+ let page = 0;
1254
+ page < SLACK_DELIVERY_RECONCILIATION_MAX_PAGES;
1255
+ page += 1
1256
+ ) {
1257
+ const url = new URL("https://slack.com/api/conversations.replies");
1258
+ url.searchParams.set("channel", channelId);
1259
+ url.searchParams.set("ts", threadTs);
1260
+ url.searchParams.set(
1261
+ "limit",
1262
+ String(SLACK_DELIVERY_RECONCILIATION_PAGE_LIMIT),
1263
+ );
1264
+ if (cursor) url.searchParams.set("cursor", cursor);
1265
+ if (typeof reconcileAfter === "number" && reconcileAfter > 0) {
1266
+ url.searchParams.set("oldest", String(Math.floor(reconcileAfter / 1000)));
1267
+ url.searchParams.set("inclusive", "true");
1268
+ }
1269
+ const body = (await slackApiJson(url.toString(), {
1270
+ headers: { Authorization: `Bearer ${token}` },
1271
+ signal,
1272
+ })) as {
1273
+ ok: boolean;
1274
+ error?: string;
1275
+ messages?: Array<{
1276
+ ts?: string;
1277
+ blocks?: Array<{ block_id?: string }>;
1278
+ }>;
1279
+ response_metadata?: { next_cursor?: string };
1280
+ };
1281
+ if (!body.ok) {
1282
+ throw new Error(body.error || "conversations.replies failed");
1283
+ }
1284
+ for (const message of body.messages ?? []) {
1285
+ if (typeof message.ts !== "string") continue;
1286
+ for (const block of message.blocks ?? []) {
1287
+ if (typeof block.block_id !== "string") continue;
1288
+ const chunkIndex = expectedMarkers.get(block.block_id);
1289
+ if (chunkIndex !== undefined) refs.set(chunkIndex, message.ts);
1290
+ }
1291
+ }
1292
+ if (refs.size === expectedMarkers.size) return refs;
1293
+ cursor = body.response_metadata?.next_cursor?.trim() ?? "";
1294
+ if (!cursor) return refs;
1295
+ }
1296
+ throw new Error(
1297
+ "Slack delivery reconciliation exceeded the bounded thread scan",
1298
+ );
1299
+ }
1300
+
1301
+ async function slackApiJson(
1134
1302
  url: string,
1135
1303
  init: RequestInit,
1136
1304
  timeoutMs = SLACK_API_TIMEOUT_MS,
1137
- ): Promise<Response> {
1305
+ ): Promise<Record<string, any>> {
1138
1306
  const controller =
1139
1307
  typeof AbortController !== "undefined" ? new AbortController() : undefined;
1308
+ const externalSignal = init.signal;
1309
+ const abortFromExternal = () => controller?.abort(externalSignal?.reason);
1310
+ if (controller && externalSignal) {
1311
+ if (externalSignal.aborted) abortFromExternal();
1312
+ else {
1313
+ externalSignal.addEventListener("abort", abortFromExternal, {
1314
+ once: true,
1315
+ });
1316
+ }
1317
+ }
1140
1318
  const timer = controller
1141
1319
  ? setTimeout(() => controller.abort(), timeoutMs)
1142
1320
  : undefined;
1143
1321
  try {
1144
- return await fetch(url, {
1322
+ const response = await fetch(url, {
1145
1323
  ...init,
1146
1324
  signal: controller?.signal ?? init.signal,
1147
1325
  });
1326
+ return (await response.json()) as Record<string, any>;
1148
1327
  } finally {
1149
1328
  if (timer) clearTimeout(timer);
1329
+ externalSignal?.removeEventListener("abort", abortFromExternal);
1150
1330
  }
1151
1331
  }
1152
1332
 
@@ -1165,12 +1345,11 @@ async function resolveSlackThreadPermalink(
1165
1345
  const url = new URL("https://slack.com/api/chat.getPermalink");
1166
1346
  url.searchParams.set("channel", channelId);
1167
1347
  url.searchParams.set("message_ts", threadTs);
1168
- const res = await slackApiFetch(
1348
+ const data = (await slackApiJson(
1169
1349
  url.toString(),
1170
1350
  { headers: { Authorization: `Bearer ${token}` } },
1171
1351
  SLACK_PERMALINK_TIMEOUT_MS,
1172
- );
1173
- const data = (await res.json()) as {
1352
+ )) as {
1174
1353
  ok?: boolean;
1175
1354
  permalink?: unknown;
1176
1355
  };
@@ -1260,12 +1439,11 @@ async function slackJson(
1260
1439
  for (const [key, value] of Object.entries(params)) {
1261
1440
  url.searchParams.set(key, value);
1262
1441
  }
1263
- const response = await slackApiFetch(
1442
+ const body = await slackApiJson(
1264
1443
  url.toString(),
1265
1444
  { headers: { Authorization: `Bearer ${token}` } },
1266
1445
  timeoutMs,
1267
1446
  );
1268
- const body = (await response.json()) as Record<string, any>;
1269
1447
  return body.ok ? body : null;
1270
1448
  } catch {
1271
1449
  return null;
@@ -1618,16 +1796,17 @@ async function postSlackJson(
1618
1796
  token: string,
1619
1797
  method: string,
1620
1798
  body: Record<string, unknown>,
1799
+ signal?: AbortSignal,
1621
1800
  ): Promise<Record<string, any>> {
1622
- const response = await slackApiFetch(`https://slack.com/api/${method}`, {
1801
+ const data = await slackApiJson(`https://slack.com/api/${method}`, {
1623
1802
  method: "POST",
1624
1803
  headers: {
1625
1804
  Authorization: `Bearer ${token}`,
1626
1805
  "Content-Type": "application/json",
1627
1806
  },
1628
1807
  body: JSON.stringify(body),
1808
+ signal,
1629
1809
  });
1630
- const data = (await response.json()) as Record<string, any>;
1631
1810
  if (!data.ok) throw new Error(data.error || `${method} failed`);
1632
1811
  return data;
1633
1812
  }
@@ -1775,6 +1954,7 @@ function createSlackRunProgress(
1775
1954
 
1776
1955
  return {
1777
1956
  ref: { kind: "slack-stream", streamTs },
1957
+ responseTargetRef: streamTs,
1778
1958
  async onEvent(event) {
1779
1959
  if (!cancelControl) {
1780
1960
  const context = getIntegrationRequestContext();
@@ -1933,7 +2113,7 @@ function createSlackRunProgress(
1933
2113
  });
1934
2114
  }
1935
2115
  },
1936
- async complete(message) {
2116
+ async complete(message, opts) {
1937
2117
  if (pendingTimer) clearTimeout(pendingTimer);
1938
2118
  const finalChunks = [...tasks.entries()].map(([id, task]) => ({
1939
2119
  type: "task_update",
@@ -1968,25 +2148,35 @@ function createSlackRunProgress(
1968
2148
  },
1969
2149
  ]
1970
2150
  : [];
1971
- await postSlackJson(token, "chat.stopStream", {
1972
- channel,
1973
- ts: streamTs,
1974
- markdown_text: message.text || "Done.",
1975
- ...(finalChunks.length ? { chunks: finalChunks } : {}),
1976
- ...(messageBlocks.length || controlBlocks.length
1977
- ? { blocks: [...messageBlocks, ...controlBlocks].slice(0, 50) }
1978
- : {}),
1979
- });
2151
+ await postSlackJson(
2152
+ token,
2153
+ "chat.stopStream",
2154
+ {
2155
+ channel,
2156
+ ts: streamTs,
2157
+ markdown_text: message.text || "Done.",
2158
+ ...(finalChunks.length ? { chunks: finalChunks } : {}),
2159
+ ...(messageBlocks.length || controlBlocks.length
2160
+ ? { blocks: [...messageBlocks, ...controlBlocks].slice(0, 50) }
2161
+ : {}),
2162
+ },
2163
+ opts?.signal,
2164
+ );
1980
2165
  setSlackAssistantStatus(token, channel, threadTs, "");
1981
2166
  return { status: "delivered", messageRefs: [streamTs] };
1982
2167
  },
1983
- async fail(message) {
2168
+ async fail(message, opts) {
1984
2169
  if (pendingTimer) clearTimeout(pendingTimer);
1985
- await postSlackJson(token, "chat.stopStream", {
1986
- channel,
1987
- ts: streamTs,
1988
- markdown_text: message.slice(0, SLACK_MAX_LENGTH),
1989
- }).catch(() => {});
2170
+ await postSlackJson(
2171
+ token,
2172
+ "chat.stopStream",
2173
+ {
2174
+ channel,
2175
+ ts: streamTs,
2176
+ markdown_text: message.slice(0, SLACK_MAX_LENGTH),
2177
+ },
2178
+ opts?.signal,
2179
+ ).catch(() => {});
1990
2180
  setSlackAssistantStatus(token, channel, threadTs, "");
1991
2181
  },
1992
2182
  };
@@ -1,3 +1,4 @@
1
+ import { getA2AContinuationTaskOutcome } from "./a2a-continuations-store.js";
1
2
  import {
2
3
  getIntegrationCampaign,
3
4
  failDisabledIntegrationCampaignTask,
@@ -72,7 +73,9 @@ export async function recoverDueIntegrationCampaigns(options: {
72
73
  : undefined,
73
74
  },
74
75
  );
75
- const confirmedReceipt = hasConfirmedDeliveryReceipt(task.payload);
76
+ const confirmedReceipt =
77
+ hasConfirmedDeliveryReceipt(task.payload) ||
78
+ (await getA2AContinuationTaskOutcome(task.id)) === "terminal-delivered";
76
79
  if (!durableDispatchEnabled && !confirmedReceipt) {
77
80
  await failDisabledIntegrationCampaignTask(task.id);
78
81
  const nextTask = await getNextPendingTaskForThread(
@@ -36,12 +36,11 @@ import { runWithRequestContext } from "../server/request-context.js";
36
36
  import {
37
37
  processA2AContinuationById,
38
38
  processDueA2AContinuations,
39
+ reconcileTerminalA2AParentIfDisabled,
40
+ recoverA2AContinuationAfterProcessorFailure,
39
41
  recoverDueA2AContinuations,
40
42
  } from "./a2a-continuation-processor.js";
41
- import {
42
- failA2AContinuation,
43
- hasActiveA2AContinuationsForIntegrationTask,
44
- } from "./a2a-continuations-store.js";
43
+ import { getA2AContinuationTaskOutcome } from "./a2a-continuations-store.js";
45
44
  import { mergeIntegrationAdapters } from "./adapter-overrides.js";
46
45
  import { discordAdapter } from "./adapters/discord.js";
47
46
  import { emailAdapter } from "./adapters/email.js";
@@ -1908,13 +1907,18 @@ export function createIntegrationsPlugin(
1908
1907
  ? { channelId: task.dispatchScope }
1909
1908
  : undefined,
1910
1909
  });
1910
+ const a2aOutcome = campaignContinuation
1911
+ ? await getA2AContinuationTaskOutcome(task.id)
1912
+ : null;
1911
1913
  const confirmedDeliveryReceipt =
1912
1914
  taskPayload.kind === "response-delivery" &&
1913
1915
  taskPayload.deliveryReceipt?.status === "delivered";
1916
+ const confirmedDeliveryProof =
1917
+ confirmedDeliveryReceipt || a2aOutcome === "terminal-delivered";
1914
1918
  if (
1915
1919
  campaignContinuation &&
1916
1920
  !durableCampaignEnabled &&
1917
- !confirmedDeliveryReceipt
1921
+ !confirmedDeliveryProof
1918
1922
  ) {
1919
1923
  await failDisabledIntegrationCampaignTask(task.id);
1920
1924
  const nextTask = await getNextPendingTaskForThread(
@@ -1995,6 +1999,17 @@ export function createIntegrationsPlugin(
1995
1999
  return;
1996
2000
  }
1997
2001
  if (taskPayload.kind === "response-delivery") {
2002
+ if (
2003
+ campaignContinuation &&
2004
+ taskPayload.awaitingA2ACompletion &&
2005
+ a2aOutcome === "terminal-delivered"
2006
+ ) {
2007
+ const completed =
2008
+ await completeIntegrationCampaignTaskAfterA2A(task.id);
2009
+ return completed
2010
+ ? ("completed" as const)
2011
+ : ("campaign-active" as const);
2012
+ }
1998
2013
  let receipt: void | PlatformDeliveryReceipt =
1999
2014
  taskPayload.deliveryReceipt;
2000
2015
  let deliveryLease:
@@ -2114,15 +2129,15 @@ export function createIntegrationsPlugin(
2114
2129
  if (!waiting) return "campaign-active" as const;
2115
2130
  }
2116
2131
  if (
2117
- !(await hasActiveA2AContinuationsForIntegrationTask(
2118
- task.id,
2119
- ))
2132
+ a2aOutcome === "terminal-without-delivery" &&
2133
+ (await reconcileTerminalA2AParentIfDisabled(task.id))
2120
2134
  ) {
2121
- const completed =
2122
- await completeIntegrationCampaignTaskAfterA2A(task.id);
2123
- return completed
2124
- ? ("completed" as const)
2125
- : ("campaign-active" as const);
2135
+ return "campaign-failed" as const;
2136
+ }
2137
+ if (a2aOutcome !== "active") {
2138
+ console.warn(
2139
+ `[integrations] Waiting campaign ${task.id} has A2A outcome ${a2aOutcome} without terminal delivery proof`,
2140
+ );
2126
2141
  }
2127
2142
  return "campaign-active" as const;
2128
2143
  }
@@ -2387,15 +2402,18 @@ export function createIntegrationsPlugin(
2387
2402
  adapters: adapterMap,
2388
2403
  });
2389
2404
  } catch (err: any) {
2390
- // Mark the continuation failed so it isn't left dangling, and surface
2391
- // a 500 to the caller instead of leaking an unhandled rejection.
2392
- await failA2AContinuation(
2393
- continuationId,
2394
- err?.message?.slice(0, 500) || "continuation processing failed",
2395
- ).catch(() => {});
2405
+ const reason =
2406
+ err?.message?.slice(0, 500) || "continuation processing failed";
2407
+ await recoverA2AContinuationAfterProcessorFailure(continuationId, {
2408
+ adapters: adapterMap,
2409
+ reason,
2410
+ }).catch(() => {
2411
+ console.error(
2412
+ `[integrations] A2A continuation ${continuationId} recovery scheduling failed`,
2413
+ );
2414
+ });
2396
2415
  console.error(
2397
- "[integrations] process-a2a-continuation failure:",
2398
- err,
2416
+ `[integrations] process-a2a-continuation failure for ${continuationId}; durable recovery requested`,
2399
2417
  );
2400
2418
  setResponseStatus(event, 500);
2401
2419
  return { error: "Failed to process A2A continuation" };