@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/run-loop-with-resume.ts +12 -0
- package/corpus/core/src/cli/index.ts +2 -1
- package/corpus/core/src/cli/mcp.ts +13 -3
- package/corpus/core/src/cli/skills-content/rewind-skill.ts +36 -4
- package/corpus/core/src/cli/skills.ts +250 -37
- package/corpus/core/src/cli/telemetry-routing.ts +57 -0
- package/corpus/core/src/cli/telemetry.ts +3 -3
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +182 -0
- package/corpus/core/src/integrations/a2a-continuations-store.ts +122 -12
- package/corpus/core/src/integrations/integration-campaign-recovery.ts +127 -0
- package/corpus/core/src/integrations/integration-campaigns-store.ts +1039 -0
- package/corpus/core/src/integrations/integration-durable-dispatch.ts +28 -3
- package/corpus/core/src/integrations/pending-tasks-retry-job.ts +8 -0
- package/corpus/core/src/integrations/plugin.ts +453 -35
- package/corpus/core/src/integrations/webhook-handler.ts +469 -11
- package/corpus/core/src/mcp/screen-memory-stdio.ts +21 -2
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +7 -3
- package/corpus/templates/content/app/components/editor/body-hydration.ts +10 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +1 -0
- package/corpus/templates/content/app/root.tsx +12 -1
- package/corpus/templates/content/changelog/2026-07-23-new-pages-open-immediately-while-slow-network-work-continues.md +6 -0
- package/corpus/templates/content/changelog/2026-07-25-database-preview-actions-open-reliably.md +6 -0
- package/corpus/templates/content/e2e/playwright.config.ts +1 -1
- package/dist/agent/run-loop-with-resume.d.ts +7 -1
- package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
- package/dist/agent/run-loop-with-resume.js +7 -0
- package/dist/agent/run-loop-with-resume.js.map +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +12 -3
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/skills-content/rewind-skill.d.ts +1 -1
- package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -1
- package/dist/cli/skills-content/rewind-skill.js +36 -4
- package/dist/cli/skills-content/rewind-skill.js.map +1 -1
- package/dist/cli/skills.d.ts +2 -0
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +191 -40
- package/dist/cli/skills.js.map +1 -1
- package/dist/cli/telemetry-routing.d.ts +2 -0
- package/dist/cli/telemetry-routing.d.ts.map +1 -0
- package/dist/cli/telemetry-routing.js +54 -0
- package/dist/cli/telemetry-routing.js.map +1 -0
- package/dist/cli/telemetry.js +3 -3
- package/dist/cli/telemetry.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +3 -3
- package/dist/integrations/a2a-continuation-processor.d.ts +14 -0
- package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +134 -1
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/integrations/a2a-continuations-store.d.ts +23 -0
- package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
- package/dist/integrations/a2a-continuations-store.js +99 -13
- package/dist/integrations/a2a-continuations-store.js.map +1 -1
- package/dist/integrations/integration-campaign-recovery.d.ts +16 -0
- package/dist/integrations/integration-campaign-recovery.d.ts.map +1 -0
- package/dist/integrations/integration-campaign-recovery.js +94 -0
- package/dist/integrations/integration-campaign-recovery.js.map +1 -0
- package/dist/integrations/integration-campaigns-store.d.ts +121 -0
- package/dist/integrations/integration-campaigns-store.d.ts.map +1 -0
- package/dist/integrations/integration-campaigns-store.js +784 -0
- package/dist/integrations/integration-campaigns-store.js.map +1 -0
- package/dist/integrations/integration-durable-dispatch.d.ts +3 -0
- package/dist/integrations/integration-durable-dispatch.d.ts.map +1 -1
- package/dist/integrations/integration-durable-dispatch.js +22 -3
- package/dist/integrations/integration-durable-dispatch.js.map +1 -1
- package/dist/integrations/pending-tasks-retry-job.d.ts.map +1 -1
- package/dist/integrations/pending-tasks-retry-job.js +6 -0
- package/dist/integrations/pending-tasks-retry-job.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +325 -18
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/webhook-handler.d.ts +16 -1
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +368 -13
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +2 -0
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
- package/dist/mcp/screen-memory-stdio.js +12 -2
- package/dist/mcp/screen-memory-stdio.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
- package/src/agent/run-loop-with-resume.ts +12 -0
- package/src/cli/index.ts +2 -1
- package/src/cli/mcp.ts +13 -3
- package/src/cli/skills-content/rewind-skill.ts +36 -4
- package/src/cli/skills.ts +250 -37
- package/src/cli/telemetry-routing.ts +57 -0
- package/src/cli/telemetry.ts +3 -3
- package/src/integrations/a2a-continuation-processor.ts +182 -0
- package/src/integrations/a2a-continuations-store.ts +122 -12
- package/src/integrations/integration-campaign-recovery.ts +127 -0
- package/src/integrations/integration-campaigns-store.ts +1039 -0
- package/src/integrations/integration-durable-dispatch.ts +28 -3
- package/src/integrations/pending-tasks-retry-job.ts +8 -0
- package/src/integrations/plugin.ts +453 -35
- package/src/integrations/webhook-handler.ts +469 -11
- 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
|
-
|
|
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: [
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
+
}
|