@agent-native/core 0.122.2 → 0.122.3

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 (71) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +6 -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/integrations/a2a-continuation-processor.ts +182 -0
  6. package/corpus/core/src/integrations/a2a-continuations-store.ts +122 -12
  7. package/corpus/core/src/integrations/integration-campaign-recovery.ts +127 -0
  8. package/corpus/core/src/integrations/integration-campaigns-store.ts +1039 -0
  9. package/corpus/core/src/integrations/integration-durable-dispatch.ts +28 -3
  10. package/corpus/core/src/integrations/pending-tasks-retry-job.ts +8 -0
  11. package/corpus/core/src/integrations/plugin.ts +453 -35
  12. package/corpus/core/src/integrations/webhook-handler.ts +469 -11
  13. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +7 -3
  14. package/corpus/templates/content/app/components/editor/body-hydration.ts +10 -0
  15. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +1 -0
  16. package/corpus/templates/content/app/root.tsx +12 -1
  17. package/corpus/templates/content/changelog/2026-07-23-new-pages-open-immediately-while-slow-network-work-continues.md +6 -0
  18. package/corpus/templates/content/changelog/2026-07-25-database-preview-actions-open-reliably.md +6 -0
  19. package/corpus/templates/content/e2e/playwright.config.ts +1 -1
  20. package/dist/agent/run-loop-with-resume.d.ts +7 -1
  21. package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
  22. package/dist/agent/run-loop-with-resume.js +7 -0
  23. package/dist/agent/run-loop-with-resume.js.map +1 -1
  24. package/dist/collab/awareness.d.ts +2 -2
  25. package/dist/collab/awareness.d.ts.map +1 -1
  26. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  27. package/dist/integrations/a2a-continuation-processor.d.ts +14 -0
  28. package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
  29. package/dist/integrations/a2a-continuation-processor.js +134 -1
  30. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  31. package/dist/integrations/a2a-continuations-store.d.ts +23 -0
  32. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  33. package/dist/integrations/a2a-continuations-store.js +99 -13
  34. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  35. package/dist/integrations/integration-campaign-recovery.d.ts +16 -0
  36. package/dist/integrations/integration-campaign-recovery.d.ts.map +1 -0
  37. package/dist/integrations/integration-campaign-recovery.js +94 -0
  38. package/dist/integrations/integration-campaign-recovery.js.map +1 -0
  39. package/dist/integrations/integration-campaigns-store.d.ts +121 -0
  40. package/dist/integrations/integration-campaigns-store.d.ts.map +1 -0
  41. package/dist/integrations/integration-campaigns-store.js +784 -0
  42. package/dist/integrations/integration-campaigns-store.js.map +1 -0
  43. package/dist/integrations/integration-durable-dispatch.d.ts +3 -0
  44. package/dist/integrations/integration-durable-dispatch.d.ts.map +1 -1
  45. package/dist/integrations/integration-durable-dispatch.js +22 -3
  46. package/dist/integrations/integration-durable-dispatch.js.map +1 -1
  47. package/dist/integrations/pending-tasks-retry-job.d.ts.map +1 -1
  48. package/dist/integrations/pending-tasks-retry-job.js +6 -0
  49. package/dist/integrations/pending-tasks-retry-job.js.map +1 -1
  50. package/dist/integrations/plugin.d.ts.map +1 -1
  51. package/dist/integrations/plugin.js +325 -18
  52. package/dist/integrations/plugin.js.map +1 -1
  53. package/dist/integrations/webhook-handler.d.ts +16 -1
  54. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  55. package/dist/integrations/webhook-handler.js +368 -13
  56. package/dist/integrations/webhook-handler.js.map +1 -1
  57. package/dist/notifications/routes.d.ts +3 -3
  58. package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
  59. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  60. package/dist/resources/handlers.d.ts +1 -1
  61. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  62. package/package.json +1 -1
  63. package/src/agent/run-loop-with-resume.ts +12 -0
  64. package/src/integrations/a2a-continuation-processor.ts +182 -0
  65. package/src/integrations/a2a-continuations-store.ts +122 -12
  66. package/src/integrations/integration-campaign-recovery.ts +127 -0
  67. package/src/integrations/integration-campaigns-store.ts +1039 -0
  68. package/src/integrations/integration-durable-dispatch.ts +28 -3
  69. package/src/integrations/pending-tasks-retry-job.ts +8 -0
  70. package/src/integrations/plugin.ts +453 -35
  71. package/src/integrations/webhook-handler.ts +469 -11
@@ -11,14 +11,14 @@
11
11
  * DELETE /_agent-native/notifications/:id — delete
12
12
  */
13
13
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
14
- error?: undefined;
15
14
  count: number;
16
15
  updated?: undefined;
16
+ error?: undefined;
17
17
  ok?: undefined;
18
18
  } | {
19
- error?: undefined;
20
19
  count?: undefined;
21
20
  updated: number;
21
+ error?: undefined;
22
22
  ok?: undefined;
23
23
  } | {
24
24
  count?: undefined;
@@ -26,9 +26,9 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
26
26
  error: string;
27
27
  ok?: undefined;
28
28
  } | {
29
- error?: undefined;
30
29
  count?: undefined;
31
30
  updated?: undefined;
32
31
  ok: boolean;
32
+ error?: undefined;
33
33
  }>>;
34
34
  //# sourceMappingURL=routes.d.ts.map
@@ -75,10 +75,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
75
75
  user: "user";
76
76
  }>>;
77
77
  }, z.core.$strip>>, {
78
- message?: undefined;
79
- id?: undefined;
80
- provider?: undefined;
81
78
  deleted?: undefined;
79
+ id?: undefined;
82
80
  found?: undefined;
83
81
  providers: {
84
82
  id: string;
@@ -90,44 +88,46 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
90
88
  updatedAt: number;
91
89
  }[];
92
90
  count: number;
91
+ provider?: undefined;
93
92
  registered?: undefined;
94
93
  label?: undefined;
95
- } | {
96
94
  message?: undefined;
97
- count?: undefined;
98
- id?: undefined;
95
+ } | {
99
96
  deleted?: undefined;
97
+ id?: undefined;
100
98
  providers?: undefined;
99
+ count?: undefined;
101
100
  found: boolean;
102
101
  provider: import("../custom-registry.js").CustomProviderConfig;
103
102
  registered?: undefined;
104
103
  label?: undefined;
105
- } | {
106
104
  message?: undefined;
107
- count?: undefined;
108
- provider?: undefined;
105
+ } | {
109
106
  deleted?: undefined;
110
107
  providers?: undefined;
108
+ count?: undefined;
109
+ provider?: undefined;
111
110
  found: boolean;
112
111
  id: string;
113
112
  registered?: undefined;
114
113
  label?: undefined;
115
- } | {
116
114
  message?: undefined;
117
- count?: undefined;
118
- provider?: undefined;
115
+ } | {
119
116
  found?: undefined;
120
117
  providers?: undefined;
118
+ count?: undefined;
119
+ provider?: undefined;
121
120
  deleted: boolean;
122
121
  id: string;
123
122
  registered?: undefined;
124
123
  label?: undefined;
124
+ message?: undefined;
125
125
  } | {
126
- count?: undefined;
127
- provider?: undefined;
128
126
  deleted?: undefined;
129
127
  found?: undefined;
130
128
  providers?: undefined;
129
+ count?: undefined;
130
+ provider?: undefined;
131
131
  registered: boolean;
132
132
  id: string;
133
133
  label: string;
@@ -311,9 +311,10 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
311
311
  notes?: string | undefined;
312
312
  scope?: "org" | "user" | undefined;
313
313
  }, {
314
+ message?: undefined;
314
315
  id?: undefined;
316
+ deleted?: undefined;
315
317
  found?: undefined;
316
- message?: undefined;
317
318
  providers: {
318
319
  id: string;
319
320
  label: string;
@@ -325,33 +326,32 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
325
326
  }[];
326
327
  count: number;
327
328
  provider?: undefined;
328
- deleted?: undefined;
329
329
  registered?: undefined;
330
330
  label?: undefined;
331
331
  } | {
332
- id?: undefined;
333
332
  message?: undefined;
334
333
  count?: undefined;
334
+ id?: undefined;
335
+ deleted?: undefined;
335
336
  providers?: undefined;
336
337
  found: boolean;
337
338
  provider: import("../custom-registry.js").CustomProviderConfig;
338
- deleted?: undefined;
339
339
  registered?: undefined;
340
340
  label?: undefined;
341
341
  } | {
342
342
  message?: undefined;
343
343
  count?: undefined;
344
+ deleted?: undefined;
344
345
  providers?: undefined;
345
346
  provider?: undefined;
346
347
  found: boolean;
347
348
  id: string;
348
- deleted?: undefined;
349
349
  registered?: undefined;
350
350
  label?: undefined;
351
351
  } | {
352
- found?: undefined;
353
352
  message?: undefined;
354
353
  count?: undefined;
354
+ found?: undefined;
355
355
  providers?: undefined;
356
356
  provider?: undefined;
357
357
  deleted: boolean;
@@ -359,11 +359,11 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
359
359
  registered?: undefined;
360
360
  label?: undefined;
361
361
  } | {
362
- found?: undefined;
363
362
  count?: undefined;
363
+ deleted?: undefined;
364
+ found?: undefined;
364
365
  providers?: undefined;
365
366
  provider?: undefined;
366
- deleted?: undefined;
367
367
  registered: boolean;
368
368
  id: string;
369
369
  label: string;
@@ -49,8 +49,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
49
49
  }>;
50
50
  /** DELETE /_agent-native/resources/:id — delete a resource */
51
51
  export declare function handleDeleteResource(event: any): Promise<{
52
- ok?: undefined;
53
52
  error: string;
53
+ ok?: undefined;
54
54
  } | {
55
55
  error?: undefined;
56
56
  ok: boolean;
@@ -27,11 +27,11 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
- error?: undefined;
31
30
  ok: boolean;
32
31
  key: string;
33
32
  baseUrlKey?: string;
34
33
  scope: AgentEngineApiKeyScope;
34
+ error?: undefined;
35
35
  }>>;
36
36
  export {};
37
37
  //# sourceMappingURL=agent-engine-api-key-route.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.122.2",
3
+ "version": "0.122.3",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -124,6 +124,18 @@ async function appendContinuationAndJournal(
124
124
  appendToolCallJournalNote(messages, events);
125
125
  }
126
126
 
127
+ /**
128
+ * Rebuild the same safe continuation context for a logical turn that resumes
129
+ * in a fresh hosted invocation.
130
+ */
131
+ export async function appendDurableContinuationContext(
132
+ messages: EngineMessage[],
133
+ reason: AgentLoopContinuationReason,
134
+ threadId: string,
135
+ ): Promise<void> {
136
+ await appendContinuationAndJournal(messages, reason, threadId);
137
+ }
138
+
127
139
  async function hasCompletedSideEffectToolCallInCurrentTurn(
128
140
  threadId: string | undefined,
129
141
  localEvents: readonly AgentChatEvent[] = [],
@@ -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(