@agent-native/core 0.122.2 → 0.122.4

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 (112) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +12 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/run-loop-with-resume.ts +12 -0
  5. package/corpus/core/src/cli/index.ts +2 -1
  6. package/corpus/core/src/cli/mcp.ts +13 -3
  7. package/corpus/core/src/cli/skills-content/rewind-skill.ts +36 -4
  8. package/corpus/core/src/cli/skills.ts +250 -37
  9. package/corpus/core/src/cli/telemetry-routing.ts +57 -0
  10. package/corpus/core/src/cli/telemetry.ts +3 -3
  11. package/corpus/core/src/integrations/a2a-continuation-processor.ts +182 -0
  12. package/corpus/core/src/integrations/a2a-continuations-store.ts +122 -12
  13. package/corpus/core/src/integrations/integration-campaign-recovery.ts +127 -0
  14. package/corpus/core/src/integrations/integration-campaigns-store.ts +1039 -0
  15. package/corpus/core/src/integrations/integration-durable-dispatch.ts +28 -3
  16. package/corpus/core/src/integrations/pending-tasks-retry-job.ts +8 -0
  17. package/corpus/core/src/integrations/plugin.ts +453 -35
  18. package/corpus/core/src/integrations/webhook-handler.ts +469 -11
  19. package/corpus/core/src/mcp/screen-memory-stdio.ts +21 -2
  20. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +7 -3
  21. package/corpus/templates/content/app/components/editor/body-hydration.ts +10 -0
  22. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +1 -0
  23. package/corpus/templates/content/app/root.tsx +12 -1
  24. package/corpus/templates/content/changelog/2026-07-23-new-pages-open-immediately-while-slow-network-work-continues.md +6 -0
  25. package/corpus/templates/content/changelog/2026-07-25-database-preview-actions-open-reliably.md +6 -0
  26. package/corpus/templates/content/e2e/playwright.config.ts +1 -1
  27. package/dist/agent/run-loop-with-resume.d.ts +7 -1
  28. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  29. package/dist/agent/run-loop-with-resume.js +7 -0
  30. package/dist/agent/run-loop-with-resume.js.map +1 -1
  31. package/dist/cli/index.js +3 -1
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/mcp.d.ts.map +1 -1
  34. package/dist/cli/mcp.js +12 -3
  35. package/dist/cli/mcp.js.map +1 -1
  36. package/dist/cli/skills-content/rewind-skill.d.ts +1 -1
  37. package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -1
  38. package/dist/cli/skills-content/rewind-skill.js +36 -4
  39. package/dist/cli/skills-content/rewind-skill.js.map +1 -1
  40. package/dist/cli/skills.d.ts +2 -0
  41. package/dist/cli/skills.d.ts.map +1 -1
  42. package/dist/cli/skills.js +191 -40
  43. package/dist/cli/skills.js.map +1 -1
  44. package/dist/cli/telemetry-routing.d.ts +2 -0
  45. package/dist/cli/telemetry-routing.d.ts.map +1 -0
  46. package/dist/cli/telemetry-routing.js +54 -0
  47. package/dist/cli/telemetry-routing.js.map +1 -0
  48. package/dist/cli/telemetry.js +3 -3
  49. package/dist/cli/telemetry.js.map +1 -1
  50. package/dist/collab/awareness.d.ts +2 -2
  51. package/dist/collab/awareness.d.ts.map +1 -1
  52. package/dist/collab/routes.d.ts +1 -1
  53. package/dist/collab/struct-routes.d.ts +1 -1
  54. package/dist/file-upload/actions/upload-image.d.ts +3 -3
  55. package/dist/integrations/a2a-continuation-processor.d.ts +14 -0
  56. package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
  57. package/dist/integrations/a2a-continuation-processor.js +134 -1
  58. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  59. package/dist/integrations/a2a-continuations-store.d.ts +23 -0
  60. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  61. package/dist/integrations/a2a-continuations-store.js +99 -13
  62. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  63. package/dist/integrations/integration-campaign-recovery.d.ts +16 -0
  64. package/dist/integrations/integration-campaign-recovery.d.ts.map +1 -0
  65. package/dist/integrations/integration-campaign-recovery.js +94 -0
  66. package/dist/integrations/integration-campaign-recovery.js.map +1 -0
  67. package/dist/integrations/integration-campaigns-store.d.ts +121 -0
  68. package/dist/integrations/integration-campaigns-store.d.ts.map +1 -0
  69. package/dist/integrations/integration-campaigns-store.js +784 -0
  70. package/dist/integrations/integration-campaigns-store.js.map +1 -0
  71. package/dist/integrations/integration-durable-dispatch.d.ts +3 -0
  72. package/dist/integrations/integration-durable-dispatch.d.ts.map +1 -1
  73. package/dist/integrations/integration-durable-dispatch.js +22 -3
  74. package/dist/integrations/integration-durable-dispatch.js.map +1 -1
  75. package/dist/integrations/pending-tasks-retry-job.d.ts.map +1 -1
  76. package/dist/integrations/pending-tasks-retry-job.js +6 -0
  77. package/dist/integrations/pending-tasks-retry-job.js.map +1 -1
  78. package/dist/integrations/plugin.d.ts.map +1 -1
  79. package/dist/integrations/plugin.js +325 -18
  80. package/dist/integrations/plugin.js.map +1 -1
  81. package/dist/integrations/webhook-handler.d.ts +16 -1
  82. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  83. package/dist/integrations/webhook-handler.js +368 -13
  84. package/dist/integrations/webhook-handler.js.map +1 -1
  85. package/dist/mcp/screen-memory-stdio.d.ts +2 -0
  86. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  87. package/dist/mcp/screen-memory-stdio.js +12 -2
  88. package/dist/mcp/screen-memory-stdio.js.map +1 -1
  89. package/dist/notifications/routes.d.ts +3 -3
  90. package/dist/observability/routes.d.ts +1 -1
  91. package/dist/progress/routes.d.ts +1 -1
  92. package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
  93. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  94. package/dist/resources/handlers.d.ts +1 -1
  95. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  96. package/package.json +1 -1
  97. package/src/agent/run-loop-with-resume.ts +12 -0
  98. package/src/cli/index.ts +2 -1
  99. package/src/cli/mcp.ts +13 -3
  100. package/src/cli/skills-content/rewind-skill.ts +36 -4
  101. package/src/cli/skills.ts +250 -37
  102. package/src/cli/telemetry-routing.ts +57 -0
  103. package/src/cli/telemetry.ts +3 -3
  104. package/src/integrations/a2a-continuation-processor.ts +182 -0
  105. package/src/integrations/a2a-continuations-store.ts +122 -12
  106. package/src/integrations/integration-campaign-recovery.ts +127 -0
  107. package/src/integrations/integration-campaigns-store.ts +1039 -0
  108. package/src/integrations/integration-durable-dispatch.ts +28 -3
  109. package/src/integrations/pending-tasks-retry-job.ts +8 -0
  110. package/src/integrations/plugin.ts +453 -35
  111. package/src/integrations/webhook-handler.ts +469 -11
  112. package/src/mcp/screen-memory-stdio.ts +21 -2
@@ -198,7 +198,7 @@ export function createCliTelemetry(options: CliTelemetryOptions): CliTelemetry {
198
198
  const publicKey = resolvePublicKey();
199
199
  const disabled = telemetryDisabled() || !publicKey;
200
200
  const endpoint = resolveEndpoint();
201
- const installId = disabled ? "" : resolveInstallId();
201
+ let installId: string | undefined = disabled ? "" : undefined;
202
202
  const runId = crypto.randomUUID();
203
203
  const inFlight = new Set<Promise<void>>();
204
204
 
@@ -211,18 +211,18 @@ export function createCliTelemetry(options: CliTelemetryOptions): CliTelemetry {
211
211
  ci: process.env.CI === "true",
212
212
  interactive: options.interactive,
213
213
  runId,
214
- installId,
215
214
  };
216
215
 
217
216
  function track(event: string, properties?: Record<string, unknown>): void {
218
217
  if (disabled) return;
218
+ installId ??= resolveInstallId();
219
219
  const body = JSON.stringify({
220
220
  publicKey,
221
221
  event,
222
222
  anonymousId: installId,
223
223
  sessionId: runId,
224
224
  timestamp: new Date().toISOString(),
225
- properties: { ...base, ...properties },
225
+ properties: { ...base, installId, ...properties },
226
226
  });
227
227
  const promise = fetch(endpoint, {
228
228
  method: "POST",
@@ -19,12 +19,30 @@ import {
19
19
  claimA2AContinuationDelivery,
20
20
  claimDueA2AContinuations,
21
21
  completeA2AContinuation,
22
+ failA2AContinuationsForIntegrationTask,
22
23
  failA2AContinuation,
23
24
  getA2AContinuation,
25
+ hasActiveA2AContinuationsForIntegrationTask,
26
+ listRecoverableA2AIntegrationTasks,
27
+ recoverDueA2AContinuationIds,
24
28
  rescheduleA2AContinuation,
25
29
  type A2AContinuation,
30
+ type RecoverableA2AIntegrationTask,
26
31
  } from "./a2a-continuations-store.js";
32
+ import {
33
+ completeIntegrationCampaignTaskAfterA2A,
34
+ failDisabledIntegrationCampaignTask,
35
+ getIntegrationCampaignForTask,
36
+ } from "./integration-campaigns-store.js";
37
+ import {
38
+ dispatchPendingIntegrationTask,
39
+ isIntegrationDurableDispatchEnabledForTask,
40
+ } from "./integration-durable-dispatch.js";
27
41
  import { signInternalToken } from "./internal-token.js";
42
+ import {
43
+ getNextPendingTaskForThread,
44
+ getPendingTask,
45
+ } from "./pending-tasks-store.js";
28
46
  import { getThreadMapping } from "./thread-mapping-store.js";
29
47
  import type {
30
48
  OutgoingMessage,
@@ -150,10 +168,62 @@ export async function processDueA2AContinuations(options: {
150
168
  }
151
169
  }
152
170
 
171
+ /**
172
+ * Durable scheduler wake-up only: make a bounded set of due/stale rows
173
+ * eligible, then invoke their normal processors. It never polls remote A2A
174
+ * tasks or runs a mutation itself, keeping the scheduled route within its
175
+ * short execution budget. Duplicate wake-ups are safe because each processor
176
+ * still takes the store's atomic claim before it can progress or deliver.
177
+ */
178
+ export async function recoverDueA2AContinuations(options?: {
179
+ limit?: number;
180
+ webhookBaseUrl?: string;
181
+ }): Promise<{ dispatched: number; failed: number }> {
182
+ const limit = options?.limit ?? 5;
183
+ const candidateTasks = await listRecoverableA2AIntegrationTasks(200);
184
+ const eligibleTaskIds: string[] = [];
185
+ for (const task of candidateTasks) {
186
+ const enabled = isIntegrationDurableDispatchEnabledForTask({
187
+ platform: task.platform,
188
+ externalThreadId: task.externalThreadId,
189
+ platformContext: task.dispatchScope
190
+ ? { channelId: task.dispatchScope }
191
+ : undefined,
192
+ });
193
+ if (enabled) {
194
+ eligibleTaskIds.push(task.id);
195
+ if (eligibleTaskIds.length >= limit) break;
196
+ } else {
197
+ await failDisabledDurableA2ATask(task);
198
+ }
199
+ }
200
+ const ids = await recoverDueA2AContinuationIds(limit, eligibleTaskIds);
201
+ let dispatched = 0;
202
+ let failed = 0;
203
+
204
+ await Promise.all(
205
+ ids.map(async (id) => {
206
+ try {
207
+ await dispatchA2AContinuation(id, options?.webhookBaseUrl);
208
+ dispatched += 1;
209
+ } catch (err) {
210
+ failed += 1;
211
+ console.error(
212
+ `[integrations] Failed to recover A2A continuation ${id}:`,
213
+ err,
214
+ );
215
+ }
216
+ }),
217
+ );
218
+
219
+ return { dispatched, failed };
220
+ }
221
+
153
222
  async function processClaimedContinuation(
154
223
  continuation: A2AContinuation,
155
224
  options: { adapters: Map<string, PlatformAdapter> },
156
225
  ): Promise<void> {
226
+ if (!(await durableContinuationScopeStillEnabled(continuation))) return;
157
227
  const adapter = options.adapters.get(continuation.platform);
158
228
  if (!adapter) {
159
229
  await failA2AContinuation(
@@ -300,6 +370,66 @@ async function processClaimedContinuation(
300
370
  );
301
371
  }
302
372
 
373
+ async function durableContinuationScopeStillEnabled(
374
+ continuation: A2AContinuation,
375
+ ): Promise<boolean> {
376
+ const campaign = await getIntegrationCampaignForTask(
377
+ continuation.integrationTaskId,
378
+ );
379
+ if (!campaign) return true;
380
+ if (campaign.status === "completed" || campaign.status === "failed") {
381
+ await failA2AContinuation(
382
+ continuation.id,
383
+ "Owning integration campaign is already terminal",
384
+ );
385
+ return false;
386
+ }
387
+ const task = await getPendingTask(continuation.integrationTaskId);
388
+ const enabled =
389
+ task?.status === "processing" &&
390
+ isIntegrationDurableDispatchEnabledForTask({
391
+ platform: task.platform,
392
+ externalThreadId: task.externalThreadId,
393
+ platformContext: task.dispatchScope
394
+ ? { channelId: task.dispatchScope }
395
+ : undefined,
396
+ });
397
+ if (enabled) return true;
398
+
399
+ await failDisabledDurableA2ATask({
400
+ id: continuation.integrationTaskId,
401
+ platform: task?.platform ?? continuation.platform,
402
+ externalThreadId: task?.externalThreadId ?? continuation.externalThreadId,
403
+ dispatchScope: task?.dispatchScope ?? null,
404
+ status: task?.status ?? "missing",
405
+ });
406
+ return false;
407
+ }
408
+
409
+ async function failDisabledDurableA2ATask(
410
+ task: RecoverableA2AIntegrationTask,
411
+ ): Promise<void> {
412
+ const message = "Durable integration campaign was disabled for this scope";
413
+ await failA2AContinuationsForIntegrationTask(task.id, message);
414
+ await failDisabledIntegrationCampaignTask(task.id, message);
415
+ const nextTask = await getNextPendingTaskForThread(
416
+ task.platform,
417
+ task.externalThreadId,
418
+ );
419
+ if (nextTask) {
420
+ await dispatchPendingIntegrationTask({
421
+ taskId: nextTask.id,
422
+ task: {
423
+ platform: task.platform,
424
+ externalThreadId: task.externalThreadId,
425
+ platformContext: nextTask.dispatchScope
426
+ ? { channelId: nextTask.dispatchScope }
427
+ : undefined,
428
+ },
429
+ });
430
+ }
431
+ }
432
+
303
433
  async function resumeA2AContinuationProgress(
304
434
  continuation: A2AContinuation,
305
435
  adapter: PlatformAdapter,
@@ -372,6 +502,7 @@ async function notifyAndFailA2AContinuation(
372
502
  reason: string,
373
503
  progress: PlatformRunProgress | null = null,
374
504
  ): Promise<void> {
505
+ if (!(await durableContinuationScopeStillEnabled(continuation))) return;
375
506
  const deliveryContinuation = await claimA2AContinuationDelivery(
376
507
  continuation.id,
377
508
  );
@@ -399,9 +530,17 @@ async function notifyAndFailA2AContinuation(
399
530
  `[integrations] Failed to notify ${deliveryContinuation.platform} about failed A2A continuation ${deliveryContinuation.id}:`,
400
531
  err,
401
532
  );
533
+ if (deliveryContinuation.attempts >= MAX_ATTEMPTS) {
534
+ await failA2AContinuation(deliveryContinuation.id, reason);
535
+ await completeParentCampaignAfterTerminalA2A(deliveryContinuation);
536
+ return;
537
+ }
538
+ await rescheduleAndRedispatchA2AContinuation(deliveryContinuation.id);
539
+ return;
402
540
  }
403
541
 
404
542
  await failA2AContinuation(deliveryContinuation.id, reason);
543
+ await completeParentCampaignAfterTerminalA2A(deliveryContinuation);
405
544
  }
406
545
 
407
546
  async function deliverAndCompleteA2AContinuation(
@@ -410,6 +549,7 @@ async function deliverAndCompleteA2AContinuation(
410
549
  text: string,
411
550
  progress: PlatformRunProgress | null = null,
412
551
  ): Promise<void> {
552
+ if (!(await durableContinuationScopeStillEnabled(continuation))) return;
413
553
  const deliveryContinuation = await claimA2AContinuationDelivery(
414
554
  continuation.id,
415
555
  );
@@ -589,6 +729,7 @@ async function completeAfterSuccessfulDelivery(
589
729
  for (let attempt = 0; attempt < COMPLETE_AFTER_DELIVERY_ATTEMPTS; attempt++) {
590
730
  try {
591
731
  await completeA2AContinuation(continuation.id);
732
+ await completeParentCampaignAfterTerminalA2A(continuation);
592
733
  return;
593
734
  } catch (err) {
594
735
  lastError = err;
@@ -602,6 +743,47 @@ async function completeAfterSuccessfulDelivery(
602
743
  );
603
744
  }
604
745
 
746
+ async function completeParentCampaignAfterTerminalA2A(
747
+ continuation: A2AContinuation,
748
+ ): Promise<void> {
749
+ const campaign = await getIntegrationCampaignForTask(
750
+ continuation.integrationTaskId,
751
+ );
752
+ if (!campaign) return;
753
+ if (
754
+ await hasActiveA2AContinuationsForIntegrationTask(
755
+ continuation.integrationTaskId,
756
+ )
757
+ ) {
758
+ return;
759
+ }
760
+ const completed = await completeIntegrationCampaignTaskAfterA2A(
761
+ continuation.integrationTaskId,
762
+ );
763
+ if (!completed) return;
764
+
765
+ const nextTask = await getNextPendingTaskForThread(
766
+ continuation.platform,
767
+ continuation.externalThreadId,
768
+ );
769
+ if (!nextTask) return;
770
+ await dispatchPendingIntegrationTask({
771
+ taskId: nextTask.id,
772
+ task: {
773
+ platform: continuation.platform,
774
+ externalThreadId: continuation.externalThreadId,
775
+ platformContext: nextTask.dispatchScope
776
+ ? { channelId: nextTask.dispatchScope }
777
+ : undefined,
778
+ },
779
+ }).catch((err) => {
780
+ console.error(
781
+ `[integrations] Failed to wake successor ${nextTask.id} after A2A parent completion:`,
782
+ err,
783
+ );
784
+ });
785
+ }
786
+
605
787
  function formatContinuationFailureMessage(
606
788
  continuation: A2AContinuation,
607
789
  reason: string,
@@ -418,6 +418,35 @@ export async function getA2AContinuationForIntegrationTask(
418
418
  return rows[0] ? rowToContinuation(rows[0] as Record<string, unknown>) : null;
419
419
  }
420
420
 
421
+ export async function hasActiveA2AContinuationsForIntegrationTask(
422
+ integrationTaskId: string,
423
+ ): Promise<boolean> {
424
+ await ensureTable();
425
+ const { rows } = await getDbExec().execute({
426
+ sql: `SELECT 1 AS active FROM integration_a2a_continuations
427
+ WHERE integration_task_id = ?
428
+ AND status IN ('pending', 'processing', 'delivering')
429
+ LIMIT 1`,
430
+ args: [integrationTaskId],
431
+ });
432
+ return rows.length > 0;
433
+ }
434
+
435
+ export async function failA2AContinuationsForIntegrationTask(
436
+ integrationTaskId: string,
437
+ errorMessage: string,
438
+ ): Promise<void> {
439
+ await ensureTable();
440
+ const now = Date.now();
441
+ await getDbExec().execute({
442
+ sql: `UPDATE integration_a2a_continuations
443
+ SET status = 'failed', error_message = ?, updated_at = ?, completed_at = ?
444
+ WHERE integration_task_id = ?
445
+ AND status IN ('pending', 'processing', 'delivering')`,
446
+ args: [errorMessage.slice(0, 2000), now, now, integrationTaskId],
447
+ });
448
+ }
449
+
421
450
  export async function getA2AContinuationsForIntegrationTaskAgent(
422
451
  integrationTaskId: string,
423
452
  agentUrl: string,
@@ -532,40 +561,121 @@ export async function claimA2AContinuation(
532
561
  export async function claimDueA2AContinuations(
533
562
  limit = 5,
534
563
  ): Promise<A2AContinuation[]> {
564
+ const ids = await recoverDueA2AContinuationIds(limit);
565
+ const claimed: A2AContinuation[] = [];
566
+ for (const id of ids) {
567
+ const continuation = await claimA2AContinuation(id);
568
+ if (continuation) claimed.push(continuation);
569
+ }
570
+ return claimed;
571
+ }
572
+
573
+ /**
574
+ * Makes stale leases eligible again and returns a bounded set of due ids.
575
+ *
576
+ * This intentionally does not claim anything. Durable schedulers use it only
577
+ * to wake the normal processor, whose atomic claim remains the sole progress
578
+ * and delivery owner under overlapping scheduler/self-dispatch executions.
579
+ */
580
+ export async function recoverDueA2AContinuationIds(
581
+ limit = 5,
582
+ integrationTaskIds?: string[],
583
+ ): Promise<string[]> {
535
584
  await ensureTable();
536
585
  const client = getDbExec();
537
586
  const now = Date.now();
538
587
  const processingCutoff = now - PROCESSING_STUCK_AFTER_MS;
539
588
  const staleNextCheckCutoff = now - PROCESSING_NEXT_CHECK_STALE_AFTER_MS;
589
+ if (integrationTaskIds && integrationTaskIds.length === 0) return [];
590
+ const taskFilter = integrationTaskIds?.length
591
+ ? ` AND integration_task_id IN (${integrationTaskIds.map(() => "?").join(", ")})`
592
+ : "";
593
+ const taskArgs = integrationTaskIds ?? [];
540
594
  // If a processor dies while holding a delivery claim, retry the final send.
541
595
  // The stale cutoff preserves the in-flight delivery guard while keeping
542
596
  // final integration replies at-least-once.
543
597
  await client.execute({
544
598
  sql: `UPDATE integration_a2a_continuations
545
599
  SET status = ?, next_check_at = ?, updated_at = ?
546
- WHERE status = 'delivering' AND updated_at <= ?`,
547
- args: ["pending", now, now, now - 5 * 60 * 1000],
600
+ WHERE status = 'delivering' AND updated_at <= ?${taskFilter}`,
601
+ args: ["pending", now, now, now - 5 * 60 * 1000, ...taskArgs],
548
602
  });
549
603
  await client.execute({
550
604
  sql: `UPDATE integration_a2a_continuations
551
605
  SET status = ?, next_check_at = ?, updated_at = ?
552
606
  WHERE status = 'processing'
553
- AND (updated_at <= ? OR next_check_at <= ?)`,
554
- args: ["pending", now, now, processingCutoff, staleNextCheckCutoff],
607
+ AND (updated_at <= ? OR next_check_at <= ?)${taskFilter}`,
608
+ args: [
609
+ "pending",
610
+ now,
611
+ now,
612
+ processingCutoff,
613
+ staleNextCheckCutoff,
614
+ ...taskArgs,
615
+ ],
555
616
  });
556
617
  const { rows } = await client.execute({
557
618
  sql: `SELECT id FROM integration_a2a_continuations
558
- WHERE status = 'pending' AND next_check_at <= ?
619
+ WHERE status = 'pending' AND next_check_at <= ?${taskFilter}
559
620
  ORDER BY next_check_at ASC
560
621
  LIMIT ?`,
561
- args: [now, limit],
622
+ args: [now, ...taskArgs, limit],
562
623
  });
563
- const claimed: A2AContinuation[] = [];
564
- for (const row of rows) {
565
- const continuation = await claimA2AContinuation(row.id as string);
566
- if (continuation) claimed.push(continuation);
567
- }
568
- return claimed;
624
+ return rows.map((row) => row.id as string);
625
+ }
626
+
627
+ export async function listRecoverableA2AIntegrationTaskIds(
628
+ limit = 50,
629
+ ): Promise<string[]> {
630
+ const tasks = await listRecoverableA2AIntegrationTasks(limit);
631
+ return tasks.map((task) => task.id);
632
+ }
633
+
634
+ export interface RecoverableA2AIntegrationTask {
635
+ id: string;
636
+ platform: string;
637
+ externalThreadId: string;
638
+ dispatchScope: string | null;
639
+ status: string;
640
+ }
641
+
642
+ /**
643
+ * Read due continuation owners and their rollout scope in one query. Recovery
644
+ * can filter the canary in memory without an N+1 pending-task lookup loop.
645
+ */
646
+ export async function listRecoverableA2AIntegrationTasks(
647
+ limit = 50,
648
+ ): Promise<RecoverableA2AIntegrationTask[]> {
649
+ await ensureTable();
650
+ const now = Date.now();
651
+ const { rows } = await getDbExec().execute({
652
+ sql: `SELECT DISTINCT c.integration_task_id, t.platform,
653
+ t.external_thread_id, t.dispatch_scope, t.status
654
+ FROM integration_a2a_continuations c
655
+ INNER JOIN integration_pending_tasks t
656
+ ON t.id = c.integration_task_id
657
+ WHERE t.status = 'processing'
658
+ AND ((c.status = 'pending' AND c.next_check_at <= ?)
659
+ OR (c.status = 'processing' AND
660
+ (c.updated_at <= ? OR c.next_check_at <= ?))
661
+ OR (c.status = 'delivering' AND c.updated_at <= ?))
662
+ ORDER BY c.integration_task_id ASC
663
+ LIMIT ?`,
664
+ args: [
665
+ now,
666
+ now - PROCESSING_STUCK_AFTER_MS,
667
+ now - PROCESSING_NEXT_CHECK_STALE_AFTER_MS,
668
+ now - 5 * 60 * 1000,
669
+ Math.max(1, Math.min(Math.floor(limit), 200)),
670
+ ],
671
+ });
672
+ return rows.map((row) => ({
673
+ id: String(row.integration_task_id),
674
+ platform: String(row.platform),
675
+ externalThreadId: String(row.external_thread_id),
676
+ dispatchScope: (row.dispatch_scope as string | null) ?? null,
677
+ status: String(row.status),
678
+ }));
569
679
  }
570
680
 
571
681
  export async function claimA2AContinuationDelivery(
@@ -0,0 +1,127 @@
1
+ import {
2
+ getIntegrationCampaign,
3
+ failDisabledIntegrationCampaignTask,
4
+ listDueIntegrationCampaignIds,
5
+ } from "./integration-campaigns-store.js";
6
+ import {
7
+ dispatchPendingIntegrationTask,
8
+ isIntegrationDurableDispatchEnabledForTask,
9
+ } from "./integration-durable-dispatch.js";
10
+ import {
11
+ getNextPendingTaskForThread,
12
+ getPendingTask,
13
+ } from "./pending-tasks-store.js";
14
+
15
+ export interface IntegrationCampaignRecoveryResult {
16
+ selected: number;
17
+ dispatched: number;
18
+ skipped: number;
19
+ failed: number;
20
+ }
21
+
22
+ function hasConfirmedDeliveryReceipt(payload: string): boolean {
23
+ try {
24
+ const parsed = JSON.parse(payload) as {
25
+ kind?: unknown;
26
+ deliveryReceipt?: { status?: unknown };
27
+ };
28
+ return (
29
+ parsed.kind === "response-delivery" &&
30
+ parsed.deliveryReceipt?.status === "delivered"
31
+ );
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Wake due campaign chunks without claiming or executing them in the sweep.
39
+ * The signed process-task endpoint owns the lease and all mutations.
40
+ */
41
+ export async function recoverDueIntegrationCampaigns(options: {
42
+ limit?: number;
43
+ event?: unknown;
44
+ webhookBaseUrl?: string;
45
+ }): Promise<IntegrationCampaignRecoveryResult> {
46
+ const ids = await listDueIntegrationCampaignIds(options.limit ?? 20);
47
+ const result: IntegrationCampaignRecoveryResult = {
48
+ selected: ids.length,
49
+ dispatched: 0,
50
+ skipped: 0,
51
+ failed: 0,
52
+ };
53
+
54
+ for (const id of ids) {
55
+ try {
56
+ const campaign = await getIntegrationCampaign(id);
57
+ if (!campaign) {
58
+ result.skipped += 1;
59
+ continue;
60
+ }
61
+ const task = await getPendingTask(campaign.integrationTaskId);
62
+ if (!task || task.status !== "processing") {
63
+ result.skipped += 1;
64
+ continue;
65
+ }
66
+ const durableDispatchEnabled = isIntegrationDurableDispatchEnabledForTask(
67
+ {
68
+ platform: task.platform,
69
+ externalThreadId: task.externalThreadId,
70
+ platformContext: task.dispatchScope
71
+ ? { channelId: task.dispatchScope }
72
+ : undefined,
73
+ },
74
+ );
75
+ const confirmedReceipt = hasConfirmedDeliveryReceipt(task.payload);
76
+ if (!durableDispatchEnabled && !confirmedReceipt) {
77
+ await failDisabledIntegrationCampaignTask(task.id);
78
+ const nextTask = await getNextPendingTaskForThread(
79
+ task.platform,
80
+ task.externalThreadId,
81
+ );
82
+ if (nextTask) {
83
+ await dispatchPendingIntegrationTask({
84
+ taskId: nextTask.id,
85
+ task: {
86
+ platform: task.platform,
87
+ externalThreadId: task.externalThreadId,
88
+ platformContext: nextTask.dispatchScope
89
+ ? { channelId: nextTask.dispatchScope }
90
+ : undefined,
91
+ },
92
+ event: options.event,
93
+ baseUrl: options.webhookBaseUrl,
94
+ });
95
+ }
96
+ result.skipped += 1;
97
+ continue;
98
+ }
99
+ const outcome = await dispatchPendingIntegrationTask({
100
+ taskId: task.id,
101
+ task: {
102
+ platform: task.platform,
103
+ externalThreadId: task.externalThreadId,
104
+ platformContext: task.dispatchScope
105
+ ? { channelId: task.dispatchScope }
106
+ : undefined,
107
+ },
108
+ event: options.event,
109
+ baseUrl: options.webhookBaseUrl,
110
+ campaignContinuation: true,
111
+ ...(confirmedReceipt && !durableDispatchEnabled
112
+ ? { allowPortableConfirmedReceiptReconciliation: true }
113
+ : {}),
114
+ });
115
+ if (outcome === "failed") result.failed += 1;
116
+ else result.dispatched += 1;
117
+ } catch (error) {
118
+ result.failed += 1;
119
+ console.error(
120
+ `[integrations] Failed to wake integration campaign ${id}:`,
121
+ error,
122
+ );
123
+ }
124
+ }
125
+
126
+ return result;
127
+ }