@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
@@ -4,6 +4,9 @@ import { isDuplicateColumnError } from "../db/migrations.js";
4
4
  let _initPromise;
5
5
  const PROCESSING_STUCK_AFTER_MS = 5 * 60 * 1000;
6
6
  const PROCESSING_NEXT_CHECK_STALE_AFTER_MS = 60 * 1000;
7
+ const TERMINAL_HISTORY_FINALIZATION_LEASE_MS = 60 * 1000;
8
+ const MAX_VERIFIED_ARTIFACT_CHECKPOINT_CHARS = 16_000;
9
+ const MAX_TERMINAL_HISTORY_PAYLOAD_CHARS = 64_000;
7
10
  // Build the CREATE SQL lazily (not at module scope) so intType() runs at
8
11
  // RUNTIME, not import time — a module-scope call breaks any consumer whose
9
12
  // db/client mock doesn't stub intType (e.g. db-admin specs).
@@ -25,6 +28,10 @@ function buildCreateSql() {
25
28
  dedupe_key TEXT,
26
29
  a2a_task_id TEXT NOT NULL,
27
30
  a2a_auth_token TEXT,
31
+ verified_artifact_checkpoint TEXT,
32
+ terminal_delivery_kind TEXT,
33
+ terminal_delivery_confirmed_at ${intType()},
34
+ terminal_history_payload TEXT,
28
35
  status TEXT NOT NULL,
29
36
  attempts ${intType()} NOT NULL DEFAULT 0,
30
37
  next_check_at ${intType()} NOT NULL,
@@ -50,6 +57,11 @@ async function ensureTable() {
50
57
  await ensureColumnExists("integration_a2a_continuations", "dedupe_key", `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS dedupe_key TEXT`);
51
58
  await ensureColumnExists("integration_a2a_continuations", "progress_ref", `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS progress_ref TEXT`);
52
59
  await ensureColumnExists("integration_a2a_continuations", "progress_ref_claimed", `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS progress_ref_claimed ${intType()} NOT NULL DEFAULT 0`);
60
+ await ensureColumnExists("integration_a2a_continuations", "verified_artifact_checkpoint", `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS verified_artifact_checkpoint TEXT`);
61
+ await ensureColumnExists("integration_a2a_continuations", "terminal_delivery_kind", `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS terminal_delivery_kind TEXT`);
62
+ await ensureColumnExists("integration_a2a_continuations", "terminal_delivery_confirmed_at", `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS terminal_delivery_confirmed_at ${intType()}`);
63
+ await ensureColumnExists("integration_a2a_continuations", "terminal_history_payload", `ALTER TABLE integration_a2a_continuations ADD COLUMN IF NOT EXISTS terminal_history_payload TEXT`);
64
+ await backfillLegacyCompletedDeliveries(client);
53
65
  await backfillProgressRefOwners(client);
54
66
  await ensureIndexExists("idx_a2a_continuations_dedupe_key", `CREATE INDEX IF NOT EXISTS idx_a2a_continuations_dedupe_key ON integration_a2a_continuations(integration_task_id, agent_url, dedupe_key)`);
55
67
  await ensureIndexExists("idx_a2a_continuations_one_progress_owner", `CREATE UNIQUE INDEX IF NOT EXISTS idx_a2a_continuations_one_progress_owner ON integration_a2a_continuations(integration_task_id) WHERE progress_ref_claimed = 1`);
@@ -64,6 +76,11 @@ async function ensureTable() {
64
76
  await addColumnIfMissing("dedupe_key", "TEXT");
65
77
  await addColumnIfMissing("progress_ref", "TEXT");
66
78
  await addColumnIfMissing("progress_ref_claimed", `${intType()} NOT NULL DEFAULT 0`);
79
+ await addColumnIfMissing("verified_artifact_checkpoint", "TEXT");
80
+ await addColumnIfMissing("terminal_delivery_kind", "TEXT");
81
+ await addColumnIfMissing("terminal_delivery_confirmed_at", intType());
82
+ await addColumnIfMissing("terminal_history_payload", "TEXT");
83
+ await backfillLegacyCompletedDeliveries(client);
67
84
  await backfillProgressRefOwners(client);
68
85
  await retryOnDdlRace(() => client.execute(`CREATE INDEX IF NOT EXISTS idx_a2a_continuations_dedupe_key ON integration_a2a_continuations(integration_task_id, agent_url, dedupe_key)`));
69
86
  await retryOnDdlRace(() => client.execute(`CREATE UNIQUE INDEX IF NOT EXISTS idx_a2a_continuations_one_progress_owner ON integration_a2a_continuations(integration_task_id) WHERE progress_ref_claimed = 1`));
@@ -75,6 +92,9 @@ async function ensureTable() {
75
92
  }
76
93
  return _initPromise;
77
94
  }
95
+ export async function ensureA2AContinuationsTable() {
96
+ await ensureTable();
97
+ }
78
98
  async function addColumnIfMissing(name, definition) {
79
99
  try {
80
100
  await retryOnDdlRace(() => getDbExec().execute(`ALTER TABLE integration_a2a_continuations ADD COLUMN ${name} ${definition}`));
@@ -109,6 +129,15 @@ async function backfillProgressRefOwners(client) {
109
129
  )
110
130
  `);
111
131
  }
132
+ async function backfillLegacyCompletedDeliveries(client) {
133
+ await client.execute(`
134
+ UPDATE integration_a2a_continuations
135
+ SET terminal_delivery_kind = 'success',
136
+ terminal_delivery_confirmed_at = COALESCE(completed_at, updated_at)
137
+ WHERE status = 'completed'
138
+ AND terminal_delivery_confirmed_at IS NULL
139
+ `);
140
+ }
112
141
  const MAX_PROGRESS_REF_KIND_CHARS = 128;
113
142
  const MAX_PROGRESS_REF_STREAM_TS_CHARS = 256;
114
143
  /**
@@ -159,6 +188,15 @@ function rowToContinuation(row) {
159
188
  dedupeKey: row.dedupe_key ?? null,
160
189
  a2aTaskId: row.a2a_task_id,
161
190
  a2aAuthToken: row.a2a_auth_token ?? null,
191
+ verifiedArtifactCheckpoint: row.verified_artifact_checkpoint ?? null,
192
+ terminalDeliveryKind: row.terminal_delivery_kind === "success" ||
193
+ row.terminal_delivery_kind === "failure"
194
+ ? row.terminal_delivery_kind
195
+ : null,
196
+ terminalDeliveryConfirmedAt: row.terminal_delivery_confirmed_at == null
197
+ ? null
198
+ : Number(row.terminal_delivery_confirmed_at),
199
+ terminalHistoryPayload: parseTerminalHistoryPayload(row.terminal_history_payload),
162
200
  status: row.status,
163
201
  attempts: Number(row.attempts ?? 0),
164
202
  nextCheckAt: Number(row.next_check_at ?? 0),
@@ -168,6 +206,36 @@ function rowToContinuation(row) {
168
206
  completedAt: row.completed_at == null ? null : Number(row.completed_at),
169
207
  };
170
208
  }
209
+ function parseTerminalHistoryPayload(value) {
210
+ if (typeof value !== "string" || !value)
211
+ return null;
212
+ try {
213
+ const parsed = JSON.parse(value);
214
+ if (typeof parsed.text !== "string" ||
215
+ typeof parsed.deliveredAt !== "string" ||
216
+ !Array.isArray(parsed.messageRefs) ||
217
+ !parsed.messageRefs.every((ref) => typeof ref === "string") ||
218
+ !Array.isArray(parsed.artifacts)) {
219
+ return null;
220
+ }
221
+ return {
222
+ text: parsed.text,
223
+ deliveredAt: parsed.deliveredAt,
224
+ messageRefs: parsed.messageRefs,
225
+ artifacts: parsed.artifacts.filter((artifact) => !!artifact &&
226
+ typeof artifact === "object" &&
227
+ !Array.isArray(artifact) &&
228
+ typeof artifact.id === "string" &&
229
+ typeof artifact.resourceType ===
230
+ "string" &&
231
+ typeof artifact.sourceAction ===
232
+ "string"),
233
+ };
234
+ }
235
+ catch {
236
+ return null;
237
+ }
238
+ }
171
239
  export async function insertA2AContinuation(input) {
172
240
  await ensureTable();
173
241
  const client = getDbExec();
@@ -291,14 +359,60 @@ export async function hasActiveA2AContinuationsForIntegrationTask(integrationTas
291
359
  });
292
360
  return rows.length > 0;
293
361
  }
362
+ export async function getA2AContinuationTaskOutcome(integrationTaskId) {
363
+ await ensureTable();
364
+ const { rows } = await getDbExec().execute({
365
+ sql: `SELECT status, terminal_delivery_confirmed_at
366
+ FROM integration_a2a_continuations
367
+ WHERE integration_task_id = ?`,
368
+ args: [integrationTaskId],
369
+ });
370
+ if (rows.length === 0)
371
+ return "missing";
372
+ if (rows.some((row) => ["pending", "processing", "delivering"].includes(String(row.status)))) {
373
+ return "active";
374
+ }
375
+ return rows.every((row) => row.terminal_delivery_confirmed_at != null)
376
+ ? "terminal-delivered"
377
+ : "terminal-without-delivery";
378
+ }
379
+ export async function hasPendingConfirmedA2ADeliveryForIntegrationTask(integrationTaskId) {
380
+ await ensureTable();
381
+ const { rows } = await getDbExec().execute({
382
+ sql: `SELECT 1 AS confirmed FROM integration_a2a_continuations
383
+ WHERE integration_task_id = ?
384
+ AND status IN ('pending', 'processing', 'delivering')
385
+ AND terminal_delivery_confirmed_at IS NOT NULL
386
+ LIMIT 1`,
387
+ args: [integrationTaskId],
388
+ });
389
+ return rows.length > 0;
390
+ }
391
+ export async function hasOnlyLegacyFailedA2AContinuationsForIntegrationTask(integrationTaskId) {
392
+ await ensureTable();
393
+ const { rows } = await getDbExec().execute({
394
+ sql: `SELECT status, terminal_delivery_kind,
395
+ terminal_delivery_confirmed_at, terminal_history_payload
396
+ FROM integration_a2a_continuations
397
+ WHERE integration_task_id = ?`,
398
+ args: [integrationTaskId],
399
+ });
400
+ return (rows.length > 0 &&
401
+ rows.every((row) => String(row.status) === "failed" &&
402
+ row.terminal_delivery_kind == null &&
403
+ row.terminal_delivery_confirmed_at == null &&
404
+ row.terminal_history_payload == null));
405
+ }
294
406
  export async function failA2AContinuationsForIntegrationTask(integrationTaskId, errorMessage) {
295
407
  await ensureTable();
296
408
  const now = Date.now();
297
409
  await getDbExec().execute({
298
410
  sql: `UPDATE integration_a2a_continuations
299
- SET status = 'failed', error_message = ?, updated_at = ?, completed_at = ?
411
+ SET status = 'failed', error_message = ?, updated_at = ?, completed_at = ?,
412
+ verified_artifact_checkpoint = NULL
300
413
  WHERE integration_task_id = ?
301
- AND status IN ('pending', 'processing', 'delivering')`,
414
+ AND status IN ('pending', 'processing', 'delivering')
415
+ AND terminal_delivery_confirmed_at IS NULL`,
302
416
  args: [errorMessage.slice(0, 2000), now, now, integrationTaskId],
303
417
  });
304
418
  }
@@ -413,7 +527,7 @@ export async function claimDueA2AContinuations(limit = 5) {
413
527
  * to wake the normal processor, whose atomic claim remains the sole progress
414
528
  * and delivery owner under overlapping scheduler/self-dispatch executions.
415
529
  */
416
- export async function recoverDueA2AContinuationIds(limit = 5, integrationTaskIds) {
530
+ export async function recoverDueA2AContinuationIds(limit = 5, integrationTaskIds, confirmedDeliveryOnly = false) {
417
531
  await ensureTable();
418
532
  const client = getDbExec();
419
533
  const now = Date.now();
@@ -425,20 +539,25 @@ export async function recoverDueA2AContinuationIds(limit = 5, integrationTaskIds
425
539
  ? ` AND integration_task_id IN (${integrationTaskIds.map(() => "?").join(", ")})`
426
540
  : "";
427
541
  const taskArgs = integrationTaskIds ?? [];
428
- // If a processor dies while holding a delivery claim, retry the final send.
429
- // The stale cutoff preserves the in-flight delivery guard while keeping
430
- // final integration replies at-least-once.
542
+ const receiptFilter = confirmedDeliveryOnly
543
+ ? " AND terminal_delivery_confirmed_at IS NOT NULL"
544
+ : "";
545
+ // If a processor dies after a provider receipt, retry history-only custody
546
+ // as soon as its short follow-up deadline passes. A pre-receipt delivery
547
+ // claim retains the longer stale cutoff before an at-least-once resend.
431
548
  await client.execute({
432
549
  sql: `UPDATE integration_a2a_continuations
433
550
  SET status = ?, next_check_at = ?, updated_at = ?
434
- WHERE status = 'delivering' AND updated_at <= ?${taskFilter}`,
435
- args: ["pending", now, now, now - 5 * 60 * 1000, ...taskArgs],
551
+ WHERE status = 'delivering'
552
+ AND ((terminal_delivery_confirmed_at IS NOT NULL AND next_check_at <= ?)
553
+ OR updated_at <= ?)${taskFilter}${receiptFilter}`,
554
+ args: ["pending", now, now, now, now - 5 * 60 * 1000, ...taskArgs],
436
555
  });
437
556
  await client.execute({
438
557
  sql: `UPDATE integration_a2a_continuations
439
558
  SET status = ?, next_check_at = ?, updated_at = ?
440
559
  WHERE status = 'processing'
441
- AND (updated_at <= ? OR next_check_at <= ?)${taskFilter}`,
560
+ AND (updated_at <= ? OR next_check_at <= ?)${taskFilter}${receiptFilter}`,
442
561
  args: [
443
562
  "pending",
444
563
  now,
@@ -450,7 +569,7 @@ export async function recoverDueA2AContinuationIds(limit = 5, integrationTaskIds
450
569
  });
451
570
  const { rows } = await client.execute({
452
571
  sql: `SELECT id FROM integration_a2a_continuations
453
- WHERE status = 'pending' AND next_check_at <= ?${taskFilter}
572
+ WHERE status = 'pending' AND next_check_at <= ?${taskFilter}${receiptFilter}
454
573
  ORDER BY next_check_at ASC
455
574
  LIMIT ?`,
456
575
  args: [now, ...taskArgs, limit],
@@ -470,7 +589,13 @@ export async function listRecoverableA2AIntegrationTasks(limit = 50) {
470
589
  const now = Date.now();
471
590
  const { rows } = await getDbExec().execute({
472
591
  sql: `SELECT DISTINCT c.integration_task_id, t.platform,
473
- t.external_thread_id, t.dispatch_scope, t.status
592
+ t.external_thread_id, t.dispatch_scope, t.status,
593
+ EXISTS (
594
+ SELECT 1 FROM integration_a2a_continuations receipt
595
+ WHERE receipt.integration_task_id = c.integration_task_id
596
+ AND receipt.terminal_delivery_confirmed_at IS NOT NULL
597
+ AND receipt.status IN ('pending', 'processing', 'delivering')
598
+ ) AS has_pending_confirmed_delivery
474
599
  FROM integration_a2a_continuations c
475
600
  INNER JOIN integration_pending_tasks t
476
601
  ON t.id = c.integration_task_id
@@ -478,13 +603,16 @@ export async function listRecoverableA2AIntegrationTasks(limit = 50) {
478
603
  AND ((c.status = 'pending' AND c.next_check_at <= ?)
479
604
  OR (c.status = 'processing' AND
480
605
  (c.updated_at <= ? OR c.next_check_at <= ?))
481
- OR (c.status = 'delivering' AND c.updated_at <= ?))
606
+ OR (c.status = 'delivering' AND
607
+ ((c.terminal_delivery_confirmed_at IS NOT NULL AND c.next_check_at <= ?)
608
+ OR c.updated_at <= ?)))
482
609
  ORDER BY c.integration_task_id ASC
483
610
  LIMIT ?`,
484
611
  args: [
485
612
  now,
486
613
  now - PROCESSING_STUCK_AFTER_MS,
487
614
  now - PROCESSING_NEXT_CHECK_STALE_AFTER_MS,
615
+ now,
488
616
  now - 5 * 60 * 1000,
489
617
  Math.max(1, Math.min(Math.floor(limit), 200)),
490
618
  ],
@@ -495,6 +623,8 @@ export async function listRecoverableA2AIntegrationTasks(limit = 50) {
495
623
  externalThreadId: String(row.external_thread_id),
496
624
  dispatchScope: row.dispatch_scope ?? null,
497
625
  status: String(row.status),
626
+ hasPendingConfirmedDelivery: row.has_pending_confirmed_delivery === true ||
627
+ Number(row.has_pending_confirmed_delivery ?? 0) === 1,
498
628
  }));
499
629
  }
500
630
  export async function claimA2AContinuationDelivery(id) {
@@ -537,6 +667,105 @@ export async function rescheduleA2AContinuation(id, delayMs) {
537
667
  args: ["pending", now + delayMs, now, id],
538
668
  });
539
669
  }
670
+ export async function retainA2AUnconfirmedDeliveryClaim(id) {
671
+ await ensureTable();
672
+ const now = Date.now();
673
+ await getDbExec().execute({
674
+ sql: `UPDATE integration_a2a_continuations
675
+ SET next_check_at = ?, updated_at = ?
676
+ WHERE id = ? AND status = 'delivering'
677
+ AND terminal_delivery_confirmed_at IS NULL`,
678
+ args: [now + PROCESSING_STUCK_AFTER_MS, now, id],
679
+ });
680
+ }
681
+ export async function saveA2AVerifiedArtifactCheckpoint(id, checkpoint) {
682
+ await ensureTable();
683
+ const normalized = checkpoint.trim();
684
+ if (!normalized)
685
+ return null;
686
+ if (normalized.length > MAX_VERIFIED_ARTIFACT_CHECKPOINT_CHARS) {
687
+ throw new Error(`Verified artifact checkpoint exceeds ${MAX_VERIFIED_ARTIFACT_CHECKPOINT_CHARS} characters`);
688
+ }
689
+ const now = Date.now();
690
+ await getDbExec().execute({
691
+ sql: `UPDATE integration_a2a_continuations
692
+ SET verified_artifact_checkpoint = ?, updated_at = ?
693
+ WHERE id = ? AND status IN ('pending', 'processing', 'delivering')`,
694
+ args: [normalized, now, id],
695
+ });
696
+ const persisted = await getA2AContinuation(id);
697
+ return persisted?.verifiedArtifactCheckpoint === normalized
698
+ ? normalized
699
+ : null;
700
+ }
701
+ export async function recordA2ATerminalDeliveryReceipt(id, kind, historyPayload, errorMessage) {
702
+ await ensureTable();
703
+ const serializedPayload = JSON.stringify(historyPayload);
704
+ if (serializedPayload.length > MAX_TERMINAL_HISTORY_PAYLOAD_CHARS) {
705
+ throw new Error(`Terminal A2A history payload exceeds ${MAX_TERMINAL_HISTORY_PAYLOAD_CHARS} characters`);
706
+ }
707
+ const now = Date.now();
708
+ await getDbExec().execute({
709
+ sql: `UPDATE integration_a2a_continuations
710
+ SET status = 'delivering', terminal_delivery_kind = ?,
711
+ terminal_delivery_confirmed_at = COALESCE(terminal_delivery_confirmed_at, ?),
712
+ terminal_history_payload = ?, next_check_at = ?, updated_at = ?,
713
+ error_message = ?
714
+ WHERE id = ?
715
+ AND status IN ('processing', 'delivering')
716
+ AND (terminal_delivery_confirmed_at IS NULL
717
+ OR terminal_delivery_kind = ?)`,
718
+ args: [
719
+ kind,
720
+ now,
721
+ serializedPayload,
722
+ now + TERMINAL_HISTORY_FINALIZATION_LEASE_MS,
723
+ now,
724
+ errorMessage?.slice(0, 2000) ?? null,
725
+ id,
726
+ kind,
727
+ ],
728
+ });
729
+ const persisted = await getA2AContinuation(id);
730
+ if (persisted?.status !== "delivering" ||
731
+ persisted.terminalDeliveryKind !== kind ||
732
+ persisted.terminalDeliveryConfirmedAt == null ||
733
+ JSON.stringify(persisted.terminalHistoryPayload) !== serializedPayload) {
734
+ throw new Error("Terminal A2A delivery confirmation did not persist");
735
+ }
736
+ return persisted;
737
+ }
738
+ export async function finalizeA2ATerminalHistory(id) {
739
+ await ensureTable();
740
+ const continuation = await getA2AContinuation(id);
741
+ if (continuation &&
742
+ (continuation.status === "completed" || continuation.status === "failed") &&
743
+ continuation.terminalDeliveryConfirmedAt != null &&
744
+ continuation.terminalHistoryPayload == null) {
745
+ return;
746
+ }
747
+ if (!continuation?.terminalDeliveryKind ||
748
+ continuation.terminalDeliveryConfirmedAt == null ||
749
+ !continuation.terminalHistoryPayload) {
750
+ throw new Error("Terminal A2A history cannot finalize without a receipt");
751
+ }
752
+ const now = Date.now();
753
+ const status = continuation.terminalDeliveryKind === "success" ? "completed" : "failed";
754
+ await getDbExec().execute({
755
+ sql: `UPDATE integration_a2a_continuations
756
+ SET status = ?, updated_at = ?, completed_at = ?, incoming_payload = ?,
757
+ a2a_auth_token = NULL, progress_ref = NULL,
758
+ verified_artifact_checkpoint = NULL, terminal_history_payload = NULL
759
+ WHERE id = ? AND status IN ('pending', 'processing', 'delivering')
760
+ AND terminal_delivery_confirmed_at IS NOT NULL`,
761
+ args: [status, now, now, "{}", id],
762
+ });
763
+ const persisted = await getA2AContinuation(id);
764
+ if (persisted?.status !== status ||
765
+ persisted.terminalHistoryPayload != null) {
766
+ throw new Error("Terminal A2A history finalization did not persist");
767
+ }
768
+ }
540
769
  export async function completeA2AContinuation(id) {
541
770
  await ensureTable();
542
771
  const client = getDbExec();
@@ -544,9 +773,13 @@ export async function completeA2AContinuation(id) {
544
773
  await client.execute({
545
774
  sql: `UPDATE integration_a2a_continuations
546
775
  SET status = ?, updated_at = ?, completed_at = ?,
547
- incoming_payload = ?, a2a_auth_token = NULL, progress_ref = NULL
776
+ incoming_payload = ?, a2a_auth_token = NULL, progress_ref = NULL,
777
+ verified_artifact_checkpoint = NULL,
778
+ terminal_delivery_kind = COALESCE(terminal_delivery_kind, 'success'),
779
+ terminal_delivery_confirmed_at = COALESCE(terminal_delivery_confirmed_at, ?),
780
+ terminal_history_payload = NULL
548
781
  WHERE id = ? AND status IN ('processing', 'delivering', 'completed')`,
549
- args: ["completed", now, now, "{}", id],
782
+ args: ["completed", now, now, "{}", now, id],
550
783
  });
551
784
  }
552
785
  export async function failA2AContinuation(id, errorMessage) {
@@ -556,7 +789,8 @@ export async function failA2AContinuation(id, errorMessage) {
556
789
  await client.execute({
557
790
  sql: `UPDATE integration_a2a_continuations
558
791
  SET status = ?, updated_at = ?, error_message = ?,
559
- incoming_payload = ?, a2a_auth_token = NULL, progress_ref = NULL
792
+ incoming_payload = ?, a2a_auth_token = NULL, progress_ref = NULL,
793
+ verified_artifact_checkpoint = NULL
560
794
  WHERE id = ? AND status <> 'completed'`,
561
795
  args: ["failed", now, errorMessage.slice(0, 2000), "{}", id],
562
796
  });