@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,4 @@
1
+ import type { A2AArtifactIdentity } from "../a2a/artifact-response.js";
1
2
  import {
2
3
  getDbExec,
3
4
  isPostgres,
@@ -15,6 +16,9 @@ import type { IncomingMessage, PlatformRunProgressRef } from "./types.js";
15
16
  let _initPromise: Promise<void> | undefined;
16
17
  const PROCESSING_STUCK_AFTER_MS = 5 * 60 * 1000;
17
18
  const PROCESSING_NEXT_CHECK_STALE_AFTER_MS = 60 * 1000;
19
+ const TERMINAL_HISTORY_FINALIZATION_LEASE_MS = 60 * 1000;
20
+ const MAX_VERIFIED_ARTIFACT_CHECKPOINT_CHARS = 16_000;
21
+ const MAX_TERMINAL_HISTORY_PAYLOAD_CHARS = 64_000;
18
22
 
19
23
  // Build the CREATE SQL lazily (not at module scope) so intType() runs at
20
24
  // RUNTIME, not import time — a module-scope call breaks any consumer whose
@@ -37,6 +41,10 @@ function buildCreateSql(): string {
37
41
  dedupe_key TEXT,
38
42
  a2a_task_id TEXT NOT NULL,
39
43
  a2a_auth_token TEXT,
44
+ verified_artifact_checkpoint TEXT,
45
+ terminal_delivery_kind TEXT,
46
+ terminal_delivery_confirmed_at ${intType()},
47
+ terminal_history_payload TEXT,
40
48
  status TEXT NOT NULL,
41
49
  attempts ${intType()} NOT NULL DEFAULT 0,
42
50
  next_check_at ${intType()} NOT NULL,
@@ -88,6 +96,27 @@ async function ensureTable(): Promise<void> {
88
96
  "progress_ref_claimed",
89
97
  `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS progress_ref_claimed ${intType()} NOT NULL DEFAULT 0`,
90
98
  );
99
+ await ensureColumnExists(
100
+ "integration_a2a_continuations",
101
+ "verified_artifact_checkpoint",
102
+ `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS verified_artifact_checkpoint TEXT`,
103
+ );
104
+ await ensureColumnExists(
105
+ "integration_a2a_continuations",
106
+ "terminal_delivery_kind",
107
+ `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS terminal_delivery_kind TEXT`,
108
+ );
109
+ await ensureColumnExists(
110
+ "integration_a2a_continuations",
111
+ "terminal_delivery_confirmed_at",
112
+ `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS terminal_delivery_confirmed_at ${intType()}`,
113
+ );
114
+ await ensureColumnExists(
115
+ "integration_a2a_continuations",
116
+ "terminal_history_payload",
117
+ `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS terminal_history_payload TEXT`,
118
+ );
119
+ await backfillLegacyCompletedDeliveries(client);
91
120
  await backfillProgressRefOwners(client);
92
121
  await ensureIndexExists(
93
122
  "idx_a2a_continuations_dedupe_key",
@@ -123,6 +152,11 @@ async function ensureTable(): Promise<void> {
123
152
  "progress_ref_claimed",
124
153
  `${intType()} NOT NULL DEFAULT 0`,
125
154
  );
155
+ await addColumnIfMissing("verified_artifact_checkpoint", "TEXT");
156
+ await addColumnIfMissing("terminal_delivery_kind", "TEXT");
157
+ await addColumnIfMissing("terminal_delivery_confirmed_at", intType());
158
+ await addColumnIfMissing("terminal_history_payload", "TEXT");
159
+ await backfillLegacyCompletedDeliveries(client);
126
160
  await backfillProgressRefOwners(client);
127
161
  await retryOnDdlRace(() =>
128
162
  client.execute(
@@ -143,6 +177,10 @@ async function ensureTable(): Promise<void> {
143
177
  return _initPromise;
144
178
  }
145
179
 
180
+ export async function ensureA2AContinuationsTable(): Promise<void> {
181
+ await ensureTable();
182
+ }
183
+
146
184
  async function addColumnIfMissing(name: string, definition: string) {
147
185
  try {
148
186
  await retryOnDdlRace(() =>
@@ -183,6 +221,18 @@ async function backfillProgressRefOwners(
183
221
  `);
184
222
  }
185
223
 
224
+ async function backfillLegacyCompletedDeliveries(
225
+ client: ReturnType<typeof getDbExec>,
226
+ ): Promise<void> {
227
+ await client.execute(`
228
+ UPDATE integration_a2a_continuations
229
+ SET terminal_delivery_kind = 'success',
230
+ terminal_delivery_confirmed_at = COALESCE(completed_at, updated_at)
231
+ WHERE status = 'completed'
232
+ AND terminal_delivery_confirmed_at IS NULL
233
+ `);
234
+ }
235
+
186
236
  export type A2AContinuationStatus =
187
237
  | "pending"
188
238
  | "processing"
@@ -190,6 +240,21 @@ export type A2AContinuationStatus =
190
240
  | "completed"
191
241
  | "failed";
192
242
 
243
+ export type A2ATerminalDeliveryKind = "success" | "failure";
244
+
245
+ export interface A2ATerminalHistoryPayload {
246
+ text: string;
247
+ deliveredAt: string;
248
+ messageRefs: string[];
249
+ artifacts: A2AArtifactIdentity[];
250
+ }
251
+
252
+ export type A2AContinuationTaskOutcome =
253
+ | "active"
254
+ | "terminal-delivered"
255
+ | "terminal-without-delivery"
256
+ | "missing";
257
+
193
258
  export interface A2AContinuation {
194
259
  id: string;
195
260
  integrationTaskId: string;
@@ -206,6 +271,10 @@ export interface A2AContinuation {
206
271
  dedupeKey: string | null;
207
272
  a2aTaskId: string;
208
273
  a2aAuthToken: string | null;
274
+ verifiedArtifactCheckpoint: string | null;
275
+ terminalDeliveryKind: A2ATerminalDeliveryKind | null;
276
+ terminalDeliveryConfirmedAt: number | null;
277
+ terminalHistoryPayload: A2ATerminalHistoryPayload | null;
209
278
  status: A2AContinuationStatus;
210
279
  attempts: number;
211
280
  nextCheckAt: number;
@@ -270,6 +339,20 @@ function rowToContinuation(row: Record<string, unknown>): A2AContinuation {
270
339
  dedupeKey: (row.dedupe_key as string | null) ?? null,
271
340
  a2aTaskId: row.a2a_task_id as string,
272
341
  a2aAuthToken: (row.a2a_auth_token as string | null) ?? null,
342
+ verifiedArtifactCheckpoint:
343
+ (row.verified_artifact_checkpoint as string | null) ?? null,
344
+ terminalDeliveryKind:
345
+ row.terminal_delivery_kind === "success" ||
346
+ row.terminal_delivery_kind === "failure"
347
+ ? row.terminal_delivery_kind
348
+ : null,
349
+ terminalDeliveryConfirmedAt:
350
+ row.terminal_delivery_confirmed_at == null
351
+ ? null
352
+ : Number(row.terminal_delivery_confirmed_at),
353
+ terminalHistoryPayload: parseTerminalHistoryPayload(
354
+ row.terminal_history_payload,
355
+ ),
273
356
  status: row.status as A2AContinuationStatus,
274
357
  attempts: Number(row.attempts ?? 0),
275
358
  nextCheckAt: Number(row.next_check_at ?? 0),
@@ -281,6 +364,42 @@ function rowToContinuation(row: Record<string, unknown>): A2AContinuation {
281
364
  };
282
365
  }
283
366
 
367
+ function parseTerminalHistoryPayload(
368
+ value: unknown,
369
+ ): A2ATerminalHistoryPayload | null {
370
+ if (typeof value !== "string" || !value) return null;
371
+ try {
372
+ const parsed = JSON.parse(value) as Partial<A2ATerminalHistoryPayload>;
373
+ if (
374
+ typeof parsed.text !== "string" ||
375
+ typeof parsed.deliveredAt !== "string" ||
376
+ !Array.isArray(parsed.messageRefs) ||
377
+ !parsed.messageRefs.every((ref) => typeof ref === "string") ||
378
+ !Array.isArray(parsed.artifacts)
379
+ ) {
380
+ return null;
381
+ }
382
+ return {
383
+ text: parsed.text,
384
+ deliveredAt: parsed.deliveredAt,
385
+ messageRefs: parsed.messageRefs,
386
+ artifacts: parsed.artifacts.filter(
387
+ (artifact): artifact is A2AArtifactIdentity =>
388
+ !!artifact &&
389
+ typeof artifact === "object" &&
390
+ !Array.isArray(artifact) &&
391
+ typeof (artifact as { id?: unknown }).id === "string" &&
392
+ typeof (artifact as { resourceType?: unknown }).resourceType ===
393
+ "string" &&
394
+ typeof (artifact as { sourceAction?: unknown }).sourceAction ===
395
+ "string",
396
+ ),
397
+ };
398
+ } catch {
399
+ return null;
400
+ }
401
+ }
402
+
284
403
  export async function insertA2AContinuation(input: {
285
404
  integrationTaskId: string;
286
405
  platform: string;
@@ -432,6 +551,67 @@ export async function hasActiveA2AContinuationsForIntegrationTask(
432
551
  return rows.length > 0;
433
552
  }
434
553
 
554
+ export async function getA2AContinuationTaskOutcome(
555
+ integrationTaskId: string,
556
+ ): Promise<A2AContinuationTaskOutcome> {
557
+ await ensureTable();
558
+ const { rows } = await getDbExec().execute({
559
+ sql: `SELECT status, terminal_delivery_confirmed_at
560
+ FROM integration_a2a_continuations
561
+ WHERE integration_task_id = ?`,
562
+ args: [integrationTaskId],
563
+ });
564
+ if (rows.length === 0) return "missing";
565
+ if (
566
+ rows.some((row) =>
567
+ ["pending", "processing", "delivering"].includes(String(row.status)),
568
+ )
569
+ ) {
570
+ return "active";
571
+ }
572
+ return rows.every((row) => row.terminal_delivery_confirmed_at != null)
573
+ ? "terminal-delivered"
574
+ : "terminal-without-delivery";
575
+ }
576
+
577
+ export async function hasPendingConfirmedA2ADeliveryForIntegrationTask(
578
+ integrationTaskId: string,
579
+ ): Promise<boolean> {
580
+ await ensureTable();
581
+ const { rows } = await getDbExec().execute({
582
+ sql: `SELECT 1 AS confirmed FROM integration_a2a_continuations
583
+ WHERE integration_task_id = ?
584
+ AND status IN ('pending', 'processing', 'delivering')
585
+ AND terminal_delivery_confirmed_at IS NOT NULL
586
+ LIMIT 1`,
587
+ args: [integrationTaskId],
588
+ });
589
+ return rows.length > 0;
590
+ }
591
+
592
+ export async function hasOnlyLegacyFailedA2AContinuationsForIntegrationTask(
593
+ integrationTaskId: string,
594
+ ): Promise<boolean> {
595
+ await ensureTable();
596
+ const { rows } = await getDbExec().execute({
597
+ sql: `SELECT status, terminal_delivery_kind,
598
+ terminal_delivery_confirmed_at, terminal_history_payload
599
+ FROM integration_a2a_continuations
600
+ WHERE integration_task_id = ?`,
601
+ args: [integrationTaskId],
602
+ });
603
+ return (
604
+ rows.length > 0 &&
605
+ rows.every(
606
+ (row) =>
607
+ String(row.status) === "failed" &&
608
+ row.terminal_delivery_kind == null &&
609
+ row.terminal_delivery_confirmed_at == null &&
610
+ row.terminal_history_payload == null,
611
+ )
612
+ );
613
+ }
614
+
435
615
  export async function failA2AContinuationsForIntegrationTask(
436
616
  integrationTaskId: string,
437
617
  errorMessage: string,
@@ -440,9 +620,11 @@ export async function failA2AContinuationsForIntegrationTask(
440
620
  const now = Date.now();
441
621
  await getDbExec().execute({
442
622
  sql: `UPDATE integration_a2a_continuations
443
- SET status = 'failed', error_message = ?, updated_at = ?, completed_at = ?
623
+ SET status = 'failed', error_message = ?, updated_at = ?, completed_at = ?,
624
+ verified_artifact_checkpoint = NULL
444
625
  WHERE integration_task_id = ?
445
- AND status IN ('pending', 'processing', 'delivering')`,
626
+ AND status IN ('pending', 'processing', 'delivering')
627
+ AND terminal_delivery_confirmed_at IS NULL`,
446
628
  args: [errorMessage.slice(0, 2000), now, now, integrationTaskId],
447
629
  });
448
630
  }
@@ -580,6 +762,7 @@ export async function claimDueA2AContinuations(
580
762
  export async function recoverDueA2AContinuationIds(
581
763
  limit = 5,
582
764
  integrationTaskIds?: string[],
765
+ confirmedDeliveryOnly = false,
583
766
  ): Promise<string[]> {
584
767
  await ensureTable();
585
768
  const client = getDbExec();
@@ -591,20 +774,25 @@ export async function recoverDueA2AContinuationIds(
591
774
  ? ` AND integration_task_id IN (${integrationTaskIds.map(() => "?").join(", ")})`
592
775
  : "";
593
776
  const taskArgs = integrationTaskIds ?? [];
594
- // If a processor dies while holding a delivery claim, retry the final send.
595
- // The stale cutoff preserves the in-flight delivery guard while keeping
596
- // final integration replies at-least-once.
777
+ const receiptFilter = confirmedDeliveryOnly
778
+ ? " AND terminal_delivery_confirmed_at IS NOT NULL"
779
+ : "";
780
+ // If a processor dies after a provider receipt, retry history-only custody
781
+ // as soon as its short follow-up deadline passes. A pre-receipt delivery
782
+ // claim retains the longer stale cutoff before an at-least-once resend.
597
783
  await client.execute({
598
784
  sql: `UPDATE integration_a2a_continuations
599
785
  SET status = ?, next_check_at = ?, updated_at = ?
600
- WHERE status = 'delivering' AND updated_at <= ?${taskFilter}`,
601
- args: ["pending", now, now, now - 5 * 60 * 1000, ...taskArgs],
786
+ WHERE status = 'delivering'
787
+ AND ((terminal_delivery_confirmed_at IS NOT NULL AND next_check_at <= ?)
788
+ OR updated_at <= ?)${taskFilter}${receiptFilter}`,
789
+ args: ["pending", now, now, now, now - 5 * 60 * 1000, ...taskArgs],
602
790
  });
603
791
  await client.execute({
604
792
  sql: `UPDATE integration_a2a_continuations
605
793
  SET status = ?, next_check_at = ?, updated_at = ?
606
794
  WHERE status = 'processing'
607
- AND (updated_at <= ? OR next_check_at <= ?)${taskFilter}`,
795
+ AND (updated_at <= ? OR next_check_at <= ?)${taskFilter}${receiptFilter}`,
608
796
  args: [
609
797
  "pending",
610
798
  now,
@@ -616,7 +804,7 @@ export async function recoverDueA2AContinuationIds(
616
804
  });
617
805
  const { rows } = await client.execute({
618
806
  sql: `SELECT id FROM integration_a2a_continuations
619
- WHERE status = 'pending' AND next_check_at <= ?${taskFilter}
807
+ WHERE status = 'pending' AND next_check_at <= ?${taskFilter}${receiptFilter}
620
808
  ORDER BY next_check_at ASC
621
809
  LIMIT ?`,
622
810
  args: [now, ...taskArgs, limit],
@@ -637,6 +825,7 @@ export interface RecoverableA2AIntegrationTask {
637
825
  externalThreadId: string;
638
826
  dispatchScope: string | null;
639
827
  status: string;
828
+ hasPendingConfirmedDelivery: boolean;
640
829
  }
641
830
 
642
831
  /**
@@ -650,7 +839,13 @@ export async function listRecoverableA2AIntegrationTasks(
650
839
  const now = Date.now();
651
840
  const { rows } = await getDbExec().execute({
652
841
  sql: `SELECT DISTINCT c.integration_task_id, t.platform,
653
- t.external_thread_id, t.dispatch_scope, t.status
842
+ t.external_thread_id, t.dispatch_scope, t.status,
843
+ EXISTS (
844
+ SELECT 1 FROM integration_a2a_continuations receipt
845
+ WHERE receipt.integration_task_id = c.integration_task_id
846
+ AND receipt.terminal_delivery_confirmed_at IS NOT NULL
847
+ AND receipt.status IN ('pending', 'processing', 'delivering')
848
+ ) AS has_pending_confirmed_delivery
654
849
  FROM integration_a2a_continuations c
655
850
  INNER JOIN integration_pending_tasks t
656
851
  ON t.id = c.integration_task_id
@@ -658,13 +853,16 @@ export async function listRecoverableA2AIntegrationTasks(
658
853
  AND ((c.status = 'pending' AND c.next_check_at <= ?)
659
854
  OR (c.status = 'processing' AND
660
855
  (c.updated_at <= ? OR c.next_check_at <= ?))
661
- OR (c.status = 'delivering' AND c.updated_at <= ?))
856
+ OR (c.status = 'delivering' AND
857
+ ((c.terminal_delivery_confirmed_at IS NOT NULL AND c.next_check_at <= ?)
858
+ OR c.updated_at <= ?)))
662
859
  ORDER BY c.integration_task_id ASC
663
860
  LIMIT ?`,
664
861
  args: [
665
862
  now,
666
863
  now - PROCESSING_STUCK_AFTER_MS,
667
864
  now - PROCESSING_NEXT_CHECK_STALE_AFTER_MS,
865
+ now,
668
866
  now - 5 * 60 * 1000,
669
867
  Math.max(1, Math.min(Math.floor(limit), 200)),
670
868
  ],
@@ -675,6 +873,9 @@ export async function listRecoverableA2AIntegrationTasks(
675
873
  externalThreadId: String(row.external_thread_id),
676
874
  dispatchScope: (row.dispatch_scope as string | null) ?? null,
677
875
  status: String(row.status),
876
+ hasPendingConfirmedDelivery:
877
+ row.has_pending_confirmed_delivery === true ||
878
+ Number(row.has_pending_confirmed_delivery ?? 0) === 1,
678
879
  }));
679
880
  }
680
881
 
@@ -723,6 +924,131 @@ export async function rescheduleA2AContinuation(
723
924
  });
724
925
  }
725
926
 
927
+ export async function retainA2AUnconfirmedDeliveryClaim(
928
+ id: string,
929
+ ): Promise<void> {
930
+ await ensureTable();
931
+ const now = Date.now();
932
+ await getDbExec().execute({
933
+ sql: `UPDATE integration_a2a_continuations
934
+ SET next_check_at = ?, updated_at = ?
935
+ WHERE id = ? AND status = 'delivering'
936
+ AND terminal_delivery_confirmed_at IS NULL`,
937
+ args: [now + PROCESSING_STUCK_AFTER_MS, now, id],
938
+ });
939
+ }
940
+
941
+ export async function saveA2AVerifiedArtifactCheckpoint(
942
+ id: string,
943
+ checkpoint: string,
944
+ ): Promise<string | null> {
945
+ await ensureTable();
946
+ const normalized = checkpoint.trim();
947
+ if (!normalized) return null;
948
+ if (normalized.length > MAX_VERIFIED_ARTIFACT_CHECKPOINT_CHARS) {
949
+ throw new Error(
950
+ `Verified artifact checkpoint exceeds ${MAX_VERIFIED_ARTIFACT_CHECKPOINT_CHARS} characters`,
951
+ );
952
+ }
953
+ const now = Date.now();
954
+ await getDbExec().execute({
955
+ sql: `UPDATE integration_a2a_continuations
956
+ SET verified_artifact_checkpoint = ?, updated_at = ?
957
+ WHERE id = ? AND status IN ('pending', 'processing', 'delivering')`,
958
+ args: [normalized, now, id],
959
+ });
960
+ const persisted = await getA2AContinuation(id);
961
+ return persisted?.verifiedArtifactCheckpoint === normalized
962
+ ? normalized
963
+ : null;
964
+ }
965
+
966
+ export async function recordA2ATerminalDeliveryReceipt(
967
+ id: string,
968
+ kind: A2ATerminalDeliveryKind,
969
+ historyPayload: A2ATerminalHistoryPayload,
970
+ errorMessage?: string,
971
+ ): Promise<A2AContinuation> {
972
+ await ensureTable();
973
+ const serializedPayload = JSON.stringify(historyPayload);
974
+ if (serializedPayload.length > MAX_TERMINAL_HISTORY_PAYLOAD_CHARS) {
975
+ throw new Error(
976
+ `Terminal A2A history payload exceeds ${MAX_TERMINAL_HISTORY_PAYLOAD_CHARS} characters`,
977
+ );
978
+ }
979
+ const now = Date.now();
980
+ await getDbExec().execute({
981
+ sql: `UPDATE integration_a2a_continuations
982
+ SET status = 'delivering', terminal_delivery_kind = ?,
983
+ terminal_delivery_confirmed_at = COALESCE(terminal_delivery_confirmed_at, ?),
984
+ terminal_history_payload = ?, next_check_at = ?, updated_at = ?,
985
+ error_message = ?
986
+ WHERE id = ?
987
+ AND status IN ('processing', 'delivering')
988
+ AND (terminal_delivery_confirmed_at IS NULL
989
+ OR terminal_delivery_kind = ?)`,
990
+ args: [
991
+ kind,
992
+ now,
993
+ serializedPayload,
994
+ now + TERMINAL_HISTORY_FINALIZATION_LEASE_MS,
995
+ now,
996
+ errorMessage?.slice(0, 2000) ?? null,
997
+ id,
998
+ kind,
999
+ ],
1000
+ });
1001
+ const persisted = await getA2AContinuation(id);
1002
+ if (
1003
+ persisted?.status !== "delivering" ||
1004
+ persisted.terminalDeliveryKind !== kind ||
1005
+ persisted.terminalDeliveryConfirmedAt == null ||
1006
+ JSON.stringify(persisted.terminalHistoryPayload) !== serializedPayload
1007
+ ) {
1008
+ throw new Error("Terminal A2A delivery confirmation did not persist");
1009
+ }
1010
+ return persisted;
1011
+ }
1012
+
1013
+ export async function finalizeA2ATerminalHistory(id: string): Promise<void> {
1014
+ await ensureTable();
1015
+ const continuation = await getA2AContinuation(id);
1016
+ if (
1017
+ continuation &&
1018
+ (continuation.status === "completed" || continuation.status === "failed") &&
1019
+ continuation.terminalDeliveryConfirmedAt != null &&
1020
+ continuation.terminalHistoryPayload == null
1021
+ ) {
1022
+ return;
1023
+ }
1024
+ if (
1025
+ !continuation?.terminalDeliveryKind ||
1026
+ continuation.terminalDeliveryConfirmedAt == null ||
1027
+ !continuation.terminalHistoryPayload
1028
+ ) {
1029
+ throw new Error("Terminal A2A history cannot finalize without a receipt");
1030
+ }
1031
+ const now = Date.now();
1032
+ const status =
1033
+ continuation.terminalDeliveryKind === "success" ? "completed" : "failed";
1034
+ await getDbExec().execute({
1035
+ sql: `UPDATE integration_a2a_continuations
1036
+ SET status = ?, updated_at = ?, completed_at = ?, incoming_payload = ?,
1037
+ a2a_auth_token = NULL, progress_ref = NULL,
1038
+ verified_artifact_checkpoint = NULL, terminal_history_payload = NULL
1039
+ WHERE id = ? AND status IN ('pending', 'processing', 'delivering')
1040
+ AND terminal_delivery_confirmed_at IS NOT NULL`,
1041
+ args: [status, now, now, "{}", id],
1042
+ });
1043
+ const persisted = await getA2AContinuation(id);
1044
+ if (
1045
+ persisted?.status !== status ||
1046
+ persisted.terminalHistoryPayload != null
1047
+ ) {
1048
+ throw new Error("Terminal A2A history finalization did not persist");
1049
+ }
1050
+ }
1051
+
726
1052
  export async function completeA2AContinuation(id: string): Promise<void> {
727
1053
  await ensureTable();
728
1054
  const client = getDbExec();
@@ -730,9 +1056,13 @@ export async function completeA2AContinuation(id: string): Promise<void> {
730
1056
  await client.execute({
731
1057
  sql: `UPDATE integration_a2a_continuations
732
1058
  SET status = ?, updated_at = ?, completed_at = ?,
733
- incoming_payload = ?, a2a_auth_token = NULL, progress_ref = NULL
1059
+ incoming_payload = ?, a2a_auth_token = NULL, progress_ref = NULL,
1060
+ verified_artifact_checkpoint = NULL,
1061
+ terminal_delivery_kind = COALESCE(terminal_delivery_kind, 'success'),
1062
+ terminal_delivery_confirmed_at = COALESCE(terminal_delivery_confirmed_at, ?),
1063
+ terminal_history_payload = NULL
734
1064
  WHERE id = ? AND status IN ('processing', 'delivering', 'completed')`,
735
- args: ["completed", now, now, "{}", id],
1065
+ args: ["completed", now, now, "{}", now, id],
736
1066
  });
737
1067
  }
738
1068
 
@@ -746,7 +1076,8 @@ export async function failA2AContinuation(
746
1076
  await client.execute({
747
1077
  sql: `UPDATE integration_a2a_continuations
748
1078
  SET status = ?, updated_at = ?, error_message = ?,
749
- incoming_payload = ?, a2a_auth_token = NULL, progress_ref = NULL
1079
+ incoming_payload = ?, a2a_auth_token = NULL, progress_ref = NULL,
1080
+ verified_artifact_checkpoint = NULL
750
1081
  WHERE id = ? AND status <> 'completed'`,
751
1082
  args: ["failed", now, errorMessage.slice(0, 2000), "{}", id],
752
1083
  });