@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +481 -122
- package/corpus/core/src/integrations/a2a-continuations-store.ts +345 -14
- package/corpus/core/src/integrations/adapters/slack.ts +243 -53
- package/corpus/core/src/integrations/integration-campaign-recovery.ts +4 -1
- package/corpus/core/src/integrations/plugin.ts +39 -21
- package/corpus/core/src/integrations/task-queue-stats.ts +146 -0
- package/corpus/core/src/integrations/types.ts +25 -4
- package/corpus/core/src/integrations/webhook-handler.ts +18 -5
- package/corpus/templates/dispatch/changelog/2026-07-26-slack-replies-recover-after-connected-app-updates.md +6 -0
- package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +15 -0
- package/corpus/templates/slides/actions/delete-design-system.ts +145 -0
- package/corpus/templates/slides/actions/list-design-systems.ts +94 -2
- package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +43 -1
- package/corpus/templates/slides/app/hooks/use-design-systems.ts +2 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +7 -0
- package/corpus/templates/slides/app/pages/DesignSystems.tsx +56 -1
- package/corpus/templates/slides/changelog/2026-07-28-delete-a-design-system-you-own-from-the-design-systems-page-.md +6 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/integrations/a2a-continuation-processor.d.ts +5 -0
- package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +293 -79
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/integrations/a2a-continuations-store.d.ts +23 -1
- package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
- package/dist/integrations/a2a-continuations-store.js +249 -15
- package/dist/integrations/a2a-continuations-store.js.map +1 -1
- package/dist/integrations/adapters/slack.d.ts.map +1 -1
- package/dist/integrations/adapters/slack.js +155 -31
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/integration-campaign-recovery.d.ts.map +1 -1
- package/dist/integrations/integration-campaign-recovery.js +3 -1
- package/dist/integrations/integration-campaign-recovery.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +29 -12
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/task-queue-stats.d.ts +12 -0
- package/dist/integrations/task-queue-stats.d.ts.map +1 -1
- package/dist/integrations/task-queue-stats.js +106 -0
- package/dist/integrations/task-queue-stats.js.map +1 -1
- package/dist/integrations/types.d.ts +24 -6
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +12 -3
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
- package/src/integrations/a2a-continuation-processor.ts +481 -122
- package/src/integrations/a2a-continuations-store.ts +345 -14
- package/src/integrations/adapters/slack.ts +243 -53
- package/src/integrations/integration-campaign-recovery.ts +4 -1
- package/src/integrations/plugin.ts +39 -21
- package/src/integrations/task-queue-stats.ts +146 -0
- package/src/integrations/types.ts +25 -4
- package/src/integrations/webhook-handler.ts +18 -5
|
@@ -5,8 +5,8 @@ import { extractThreadMeta } from "../agent/thread-data-builder.js";
|
|
|
5
5
|
import { getThread, updateThreadData } from "../chat-threads/store.js";
|
|
6
6
|
import { withConfiguredAppBasePath } from "../server/app-base-path.js";
|
|
7
7
|
import { FRAMEWORK_ROUTE_PREFIX } from "../server/core-routes-plugin.js";
|
|
8
|
-
import { claimA2AContinuation, claimA2AContinuationDelivery, claimDueA2AContinuations,
|
|
9
|
-
import { completeIntegrationCampaignTaskAfterA2A, failDisabledIntegrationCampaignTask, getIntegrationCampaignForTask, } from "./integration-campaigns-store.js";
|
|
8
|
+
import { claimA2AContinuation, claimA2AContinuationDelivery, claimDueA2AContinuations, failA2AContinuationsForIntegrationTask, failA2AContinuation, finalizeA2ATerminalHistory, getA2AContinuation, getA2AContinuationTaskOutcome, hasOnlyLegacyFailedA2AContinuationsForIntegrationTask, hasPendingConfirmedA2ADeliveryForIntegrationTask, listRecoverableA2AIntegrationTasks, recoverDueA2AContinuationIds, recordA2ATerminalDeliveryReceipt, retainA2AUnconfirmedDeliveryClaim, rescheduleA2AContinuation, saveA2AVerifiedArtifactCheckpoint, } from "./a2a-continuations-store.js";
|
|
9
|
+
import { completeIntegrationCampaignTaskAfterA2A, failDisabledIntegrationCampaignTask, failIntegrationCampaignTaskDeliveryContainment, getIntegrationCampaignForTask, } from "./integration-campaigns-store.js";
|
|
10
10
|
import { dispatchPendingIntegrationTask, isIntegrationDurableDispatchEnabledForTask, } from "./integration-durable-dispatch.js";
|
|
11
11
|
import { signInternalToken } from "./internal-token.js";
|
|
12
12
|
import { getNextPendingTaskForThread, getPendingTask, } from "./pending-tasks-store.js";
|
|
@@ -26,6 +26,16 @@ const POLL_REQUEST_TIMEOUT_MS = 8_000;
|
|
|
26
26
|
const PLATFORM_SEND_TIMEOUT_MS = 12_000;
|
|
27
27
|
const DISPATCH_SETTLE_WAIT_MS = 2_000;
|
|
28
28
|
const COMPLETE_AFTER_DELIVERY_ATTEMPTS = 3;
|
|
29
|
+
function logA2AContinuationTransition(event, continuation) {
|
|
30
|
+
console.info("[integrations] a2a-continuation-transition", {
|
|
31
|
+
event,
|
|
32
|
+
continuationId: continuation.id,
|
|
33
|
+
integrationTaskId: continuation.integrationTaskId,
|
|
34
|
+
downstreamTaskId: continuation.a2aTaskId,
|
|
35
|
+
attempt: continuation.attempts,
|
|
36
|
+
status: continuation.status,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
29
39
|
export async function dispatchA2AContinuation(continuationId, webhookBaseUrl) {
|
|
30
40
|
const baseUrl = webhookBaseUrl ||
|
|
31
41
|
process.env.WEBHOOK_BASE_URL ||
|
|
@@ -87,10 +97,74 @@ export async function processA2AContinuationById(continuationId, options) {
|
|
|
87
97
|
return;
|
|
88
98
|
await processClaimedContinuation(continuation, options);
|
|
89
99
|
}
|
|
100
|
+
export async function recoverA2AContinuationAfterProcessorFailure(continuationId, options) {
|
|
101
|
+
const continuation = await getA2AContinuation(continuationId);
|
|
102
|
+
if (!continuation ||
|
|
103
|
+
continuation.status === "completed" ||
|
|
104
|
+
continuation.status === "failed") {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (continuation.terminalDeliveryConfirmedAt != null &&
|
|
108
|
+
continuation.terminalHistoryPayload) {
|
|
109
|
+
await persistAndFinalizeConfirmedA2ADelivery(continuation);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const adapter = options.adapters.get(continuation.platform);
|
|
113
|
+
if (continuation.attempts < MAX_ATTEMPTS) {
|
|
114
|
+
logA2AContinuationTransition("processor_released", continuation);
|
|
115
|
+
await rescheduleAndRedispatchA2AContinuation(continuation.id);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!adapter) {
|
|
119
|
+
const reason = `Unknown platform: ${continuation.platform}`;
|
|
120
|
+
logA2AContinuationTransition("processor_exhausted_without_adapter", continuation);
|
|
121
|
+
await failA2AContinuationsForIntegrationTask(continuation.integrationTaskId, reason);
|
|
122
|
+
if (await hasPendingConfirmedA2ADeliveryForIntegrationTask(continuation.integrationTaskId)) {
|
|
123
|
+
await dispatchPendingIntegrationTask({
|
|
124
|
+
taskId: continuation.integrationTaskId,
|
|
125
|
+
task: {
|
|
126
|
+
platform: continuation.platform,
|
|
127
|
+
externalThreadId: continuation.externalThreadId,
|
|
128
|
+
platformContext: continuation.incoming.platformContext,
|
|
129
|
+
},
|
|
130
|
+
campaignContinuation: true,
|
|
131
|
+
allowPortableConfirmedReceiptReconciliation: true,
|
|
132
|
+
}).catch((err) => {
|
|
133
|
+
console.error(`[integrations] Failed to wake confirmed sibling history for ${continuation.integrationTaskId}:`, err);
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
await failIntegrationCampaignTaskDeliveryContainment(continuation.integrationTaskId, reason);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const progress = await resumeA2AContinuationProgress(continuation, adapter);
|
|
141
|
+
if (continuation.verifiedArtifactCheckpoint) {
|
|
142
|
+
logA2AContinuationTransition("processor_exhausted_checkpoint_delivery", continuation);
|
|
143
|
+
await deliverAndCompleteA2AContinuation(continuation, adapter, formatRecoverableArtifactFallbackText(continuation.verifiedArtifactCheckpoint), progress);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
logA2AContinuationTransition("processor_exhausted_failure_notice", continuation);
|
|
147
|
+
await notifyAndFailA2AContinuation(continuation, adapter, options.reason, progress);
|
|
148
|
+
}
|
|
90
149
|
export async function processDueA2AContinuations(options) {
|
|
91
150
|
const continuations = await claimDueA2AContinuations(options.limit ?? 5);
|
|
92
151
|
for (const continuation of continuations) {
|
|
93
|
-
await processClaimedContinuation(continuation, options).catch((err) =>
|
|
152
|
+
await processClaimedContinuation(continuation, options).catch(async (err) => {
|
|
153
|
+
console.error(`[integrations] A2A continuation ${continuation.id} failed; durable recovery requested`);
|
|
154
|
+
try {
|
|
155
|
+
await recoverA2AContinuationAfterProcessorFailure(continuation.id, {
|
|
156
|
+
adapters: options.adapters,
|
|
157
|
+
reason: err instanceof Error
|
|
158
|
+
? err.message.slice(0, 500)
|
|
159
|
+
: "continuation processing failed",
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (recoveryError) {
|
|
163
|
+
console.error(`[integrations] A2A continuation ${continuation.id} recovery failed; later continuations will continue`, recoveryError instanceof Error
|
|
164
|
+
? recoveryError.name
|
|
165
|
+
: "recovery_error");
|
|
166
|
+
}
|
|
167
|
+
});
|
|
94
168
|
}
|
|
95
169
|
}
|
|
96
170
|
/**
|
|
@@ -104,6 +178,7 @@ export async function recoverDueA2AContinuations(options) {
|
|
|
104
178
|
const limit = options?.limit ?? 5;
|
|
105
179
|
const candidateTasks = await listRecoverableA2AIntegrationTasks(200);
|
|
106
180
|
const eligibleTaskIds = [];
|
|
181
|
+
const confirmedHistoryTaskIds = [];
|
|
107
182
|
for (const task of candidateTasks) {
|
|
108
183
|
const enabled = isIntegrationDurableDispatchEnabledForTask({
|
|
109
184
|
platform: task.platform,
|
|
@@ -114,14 +189,22 @@ export async function recoverDueA2AContinuations(options) {
|
|
|
114
189
|
});
|
|
115
190
|
if (enabled) {
|
|
116
191
|
eligibleTaskIds.push(task.id);
|
|
117
|
-
|
|
118
|
-
|
|
192
|
+
}
|
|
193
|
+
else if (task.hasPendingConfirmedDelivery) {
|
|
194
|
+
confirmedHistoryTaskIds.push(task.id);
|
|
119
195
|
}
|
|
120
196
|
else {
|
|
121
197
|
await failDisabledDurableA2ATask(task);
|
|
122
198
|
}
|
|
199
|
+
if (eligibleTaskIds.length + confirmedHistoryTaskIds.length >= limit)
|
|
200
|
+
break;
|
|
123
201
|
}
|
|
124
202
|
const ids = await recoverDueA2AContinuationIds(limit, eligibleTaskIds);
|
|
203
|
+
const remaining = Math.max(0, limit - ids.length);
|
|
204
|
+
const confirmedHistoryIds = remaining > 0 && confirmedHistoryTaskIds.length > 0
|
|
205
|
+
? await recoverDueA2AContinuationIds(remaining, confirmedHistoryTaskIds, true)
|
|
206
|
+
: [];
|
|
207
|
+
ids.push(...confirmedHistoryIds);
|
|
125
208
|
let dispatched = 0;
|
|
126
209
|
let failed = 0;
|
|
127
210
|
await Promise.all(ids.map(async (id) => {
|
|
@@ -137,6 +220,11 @@ export async function recoverDueA2AContinuations(options) {
|
|
|
137
220
|
return { dispatched, failed };
|
|
138
221
|
}
|
|
139
222
|
async function processClaimedContinuation(continuation, options) {
|
|
223
|
+
if (continuation.terminalDeliveryConfirmedAt != null &&
|
|
224
|
+
continuation.terminalHistoryPayload) {
|
|
225
|
+
await persistAndFinalizeConfirmedA2ADelivery(continuation);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
140
228
|
if (!(await durableContinuationScopeStillEnabled(continuation)))
|
|
141
229
|
return;
|
|
142
230
|
const adapter = options.adapters.get(continuation.platform);
|
|
@@ -153,13 +241,16 @@ async function processClaimedContinuation(continuation, options) {
|
|
|
153
241
|
const deadline = Date.now() + PROCESSOR_WAIT_MS;
|
|
154
242
|
const recoverableArtifactSecrets = await resolveContinuationArtifactSecrets(continuation);
|
|
155
243
|
let task = null;
|
|
156
|
-
let latestRecoverableArtifactText =
|
|
244
|
+
let latestRecoverableArtifactText = continuation.verifiedArtifactCheckpoint;
|
|
157
245
|
try {
|
|
158
246
|
while (Date.now() < deadline) {
|
|
159
247
|
task = await client.getTask(continuation.a2aTaskId);
|
|
160
248
|
const recoverableArtifactText = extractVerifiedRecoverableArtifactText(task, continuation.agentUrl, recoverableArtifactSecrets);
|
|
161
249
|
if (recoverableArtifactText) {
|
|
162
|
-
latestRecoverableArtifactText = recoverableArtifactText;
|
|
250
|
+
latestRecoverableArtifactText = await saveA2AVerifiedArtifactCheckpoint(continuation.id, recoverableArtifactText);
|
|
251
|
+
if (latestRecoverableArtifactText) {
|
|
252
|
+
logA2AContinuationTransition("checkpoint_persisted", continuation);
|
|
253
|
+
}
|
|
163
254
|
}
|
|
164
255
|
if (TERMINAL_STATES.has(task.status.state))
|
|
165
256
|
break;
|
|
@@ -181,6 +272,10 @@ async function processClaimedContinuation(continuation, options) {
|
|
|
181
272
|
return;
|
|
182
273
|
}
|
|
183
274
|
if (continuation.attempts >= MAX_ATTEMPTS) {
|
|
275
|
+
if (latestRecoverableArtifactText) {
|
|
276
|
+
await deliverAndCompleteA2AContinuation(continuation, adapter, formatRecoverableArtifactFallbackText(latestRecoverableArtifactText), progress);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
184
279
|
await notifyAndFailA2AContinuation(continuation, adapter, err instanceof Error ? err.message : String(err), progress);
|
|
185
280
|
return;
|
|
186
281
|
}
|
|
@@ -239,6 +334,11 @@ async function durableContinuationScopeStillEnabled(continuation) {
|
|
|
239
334
|
});
|
|
240
335
|
if (enabled)
|
|
241
336
|
return true;
|
|
337
|
+
if (await hasPendingConfirmedA2ADeliveryForIntegrationTask(continuation.integrationTaskId)) {
|
|
338
|
+
await failA2AContinuation(continuation.id, "Durable integration campaign was disabled before this continuation delivered");
|
|
339
|
+
await reconcileTerminalA2AParentIfDisabled(continuation.integrationTaskId);
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
242
342
|
await failDisabledDurableA2ATask({
|
|
243
343
|
id: continuation.integrationTaskId,
|
|
244
344
|
platform: task?.platform ?? continuation.platform,
|
|
@@ -266,6 +366,35 @@ async function failDisabledDurableA2ATask(task) {
|
|
|
266
366
|
});
|
|
267
367
|
}
|
|
268
368
|
}
|
|
369
|
+
export async function reconcileTerminalA2AParentIfDisabled(integrationTaskId) {
|
|
370
|
+
if ((await getA2AContinuationTaskOutcome(integrationTaskId)) !==
|
|
371
|
+
"terminal-without-delivery") {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
if (await hasOnlyLegacyFailedA2AContinuationsForIntegrationTask(integrationTaskId)) {
|
|
375
|
+
await failIntegrationCampaignTaskDeliveryContainment(integrationTaskId, "Legacy A2A continuation ended without durable delivery proof");
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
const task = await getPendingTask(integrationTaskId);
|
|
379
|
+
if (!task ||
|
|
380
|
+
isIntegrationDurableDispatchEnabledForTask({
|
|
381
|
+
platform: task.platform,
|
|
382
|
+
externalThreadId: task.externalThreadId,
|
|
383
|
+
platformContext: task.dispatchScope
|
|
384
|
+
? { channelId: task.dispatchScope }
|
|
385
|
+
: undefined,
|
|
386
|
+
})) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
await failDisabledDurableA2ATask({
|
|
390
|
+
id: task.id,
|
|
391
|
+
platform: task.platform,
|
|
392
|
+
externalThreadId: task.externalThreadId,
|
|
393
|
+
dispatchScope: task.dispatchScope,
|
|
394
|
+
status: task.status,
|
|
395
|
+
});
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
269
398
|
async function resumeA2AContinuationProgress(continuation, adapter) {
|
|
270
399
|
if (!continuation.progressRef || !adapter.resumeRunProgress)
|
|
271
400
|
return null;
|
|
@@ -322,23 +451,23 @@ async function notifyAndFailA2AContinuation(continuation, adapter, reason, progr
|
|
|
322
451
|
const deliveryContinuation = await claimA2AContinuationDelivery(continuation.id);
|
|
323
452
|
if (!deliveryContinuation)
|
|
324
453
|
return;
|
|
454
|
+
logA2AContinuationTransition("failure_delivery_claimed", deliveryContinuation);
|
|
325
455
|
const message = formatContinuationFailureMessage(deliveryContinuation, reason);
|
|
456
|
+
let outgoing;
|
|
457
|
+
let deliveryReceipt;
|
|
326
458
|
try {
|
|
327
|
-
|
|
328
|
-
await withTimeout(deliverA2AContinuationResponse(adapter, deliveryContinuation, outgoing, progress, "error"), PLATFORM_SEND_TIMEOUT_MS, `${deliveryContinuation.platform} failure notification timed out`);
|
|
459
|
+
outgoing = adapter.formatAgentResponse(message);
|
|
460
|
+
deliveryReceipt = await withTimeout((signal) => deliverA2AContinuationResponse(adapter, deliveryContinuation, outgoing, progress, "error", signal), PLATFORM_SEND_TIMEOUT_MS, `${deliveryContinuation.platform} failure notification timed out`);
|
|
329
461
|
}
|
|
330
462
|
catch (err) {
|
|
331
463
|
console.error(`[integrations] Failed to notify ${deliveryContinuation.platform} about failed A2A continuation ${deliveryContinuation.id}:`, err);
|
|
332
|
-
if (deliveryContinuation.attempts >= MAX_ATTEMPTS) {
|
|
333
|
-
await failA2AContinuation(deliveryContinuation.id, reason);
|
|
334
|
-
await completeParentCampaignAfterTerminalA2A(deliveryContinuation);
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
464
|
await rescheduleAndRedispatchA2AContinuation(deliveryContinuation.id);
|
|
338
465
|
return;
|
|
339
466
|
}
|
|
340
|
-
await
|
|
341
|
-
|
|
467
|
+
const confirmed = await recordTerminalA2ADelivery(deliveryContinuation, "failure", outgoing, deliveryReceipt, [], reason);
|
|
468
|
+
if (!confirmed)
|
|
469
|
+
return;
|
|
470
|
+
await persistAndFinalizeConfirmedA2ADelivery(confirmed);
|
|
342
471
|
}
|
|
343
472
|
async function deliverAndCompleteA2AContinuation(continuation, adapter, text, progress = null) {
|
|
344
473
|
if (!(await durableContinuationScopeStillEnabled(continuation)))
|
|
@@ -346,77 +475,76 @@ async function deliverAndCompleteA2AContinuation(continuation, adapter, text, pr
|
|
|
346
475
|
const deliveryContinuation = await claimA2AContinuationDelivery(continuation.id);
|
|
347
476
|
if (!deliveryContinuation)
|
|
348
477
|
return;
|
|
478
|
+
logA2AContinuationTransition("response_delivery_claimed", deliveryContinuation);
|
|
479
|
+
let outgoing;
|
|
480
|
+
let deliveryReceipt;
|
|
481
|
+
const artifactSecrets = await resolveContinuationArtifactSecrets(deliveryContinuation);
|
|
482
|
+
const artifacts = extractA2AArtifactIdentities([{ tool: "call-agent", result: text }], { persistedArtifactSecrets: artifactSecrets });
|
|
349
483
|
try {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
355
|
-
try {
|
|
356
|
-
await persistA2AContinuationDelivery(deliveryContinuation, outgoing, deliveryReceipt, text, artifactSecrets);
|
|
357
|
-
persistenceError = undefined;
|
|
358
|
-
break;
|
|
359
|
-
}
|
|
360
|
-
catch (err) {
|
|
361
|
-
persistenceError = err;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
if (persistenceError) {
|
|
365
|
-
console.error(`[integrations] Delivered A2A continuation ${deliveryContinuation.id} but could not persist its thread history:`, persistenceError);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
catch (err) {
|
|
369
|
-
if (deliveryContinuation.attempts >= MAX_ATTEMPTS) {
|
|
370
|
-
await failA2AContinuation(deliveryContinuation.id, err instanceof Error ? err.message : String(err));
|
|
484
|
+
outgoing = adapter.formatAgentResponse(stripA2APersistedArtifactMarkers(text));
|
|
485
|
+
deliveryReceipt = await withTimeout((signal) => deliverA2AContinuationResponse(adapter, deliveryContinuation, outgoing, progress, "done", signal), PLATFORM_SEND_TIMEOUT_MS, `${deliveryContinuation.platform} response delivery timed out`);
|
|
486
|
+
const confirmed = await recordTerminalA2ADelivery(deliveryContinuation, "success", outgoing, deliveryReceipt, artifacts);
|
|
487
|
+
if (!confirmed)
|
|
371
488
|
return;
|
|
372
|
-
|
|
489
|
+
await persistAndFinalizeConfirmedA2ADelivery(confirmed);
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
373
492
|
await rescheduleAndRedispatchA2AContinuation(deliveryContinuation.id);
|
|
374
493
|
return;
|
|
375
494
|
}
|
|
376
|
-
await completeAfterSuccessfulDelivery(deliveryContinuation);
|
|
377
495
|
}
|
|
378
|
-
async function deliverA2AContinuationResponse(adapter, continuation, message, progress, status) {
|
|
496
|
+
async function deliverA2AContinuationResponse(adapter, continuation, message, progress, status, signal) {
|
|
379
497
|
if (progress) {
|
|
380
498
|
try {
|
|
381
499
|
await progress.onEvent({
|
|
382
500
|
type: "agent_call",
|
|
383
501
|
agent: continuation.agentName,
|
|
384
502
|
status,
|
|
385
|
-
});
|
|
386
|
-
|
|
503
|
+
}, { signal });
|
|
504
|
+
throwIfAborted(signal);
|
|
505
|
+
const receipt = await progress.complete(message, { signal });
|
|
387
506
|
if (receipt?.status === "delivered")
|
|
388
507
|
return receipt;
|
|
389
508
|
throw new Error("Continuation progress completed without delivery proof");
|
|
390
509
|
}
|
|
391
510
|
catch {
|
|
511
|
+
throwIfAborted(signal);
|
|
392
512
|
// A resumed Slack stream can no longer be finalized (for example when
|
|
393
513
|
// chat.stopStream rejects). Preserve the final answer with the same
|
|
394
514
|
// thread reply fallback used by the initial webhook run. Also ask the
|
|
395
515
|
// adapter to terminate the native stream: otherwise Slack can keep the
|
|
396
516
|
// task card in its working state after the thread fallback succeeds.
|
|
397
517
|
try {
|
|
398
|
-
await progress.fail?.("I couldn't update the live response, but I posted the final result in this thread.");
|
|
518
|
+
await progress.fail?.("I couldn't update the live response, but I posted the final result in this thread.", { signal });
|
|
399
519
|
}
|
|
400
520
|
catch {
|
|
401
521
|
// The thread reply below is still the authoritative final answer.
|
|
402
522
|
}
|
|
523
|
+
logA2AContinuationTransition("native_progress_fallback", continuation);
|
|
403
524
|
}
|
|
404
525
|
}
|
|
526
|
+
throwIfAborted(signal);
|
|
405
527
|
const receipt = await adapter.sendResponse(message, continuation.incoming, {
|
|
406
|
-
|
|
528
|
+
idempotencyKey: `a2a-continuation:${continuation.id}`,
|
|
529
|
+
reconcileAfter: continuation.createdAt,
|
|
530
|
+
signal,
|
|
531
|
+
placeholderRef: progress?.responseTargetRef ?? continuation.placeholderRef ?? undefined,
|
|
532
|
+
strictTargetRef: true,
|
|
407
533
|
});
|
|
408
534
|
if (receipt?.status !== "delivered") {
|
|
409
535
|
throw new Error("Continuation response completed without delivery proof");
|
|
410
536
|
}
|
|
411
537
|
return receipt;
|
|
412
538
|
}
|
|
413
|
-
async function persistA2AContinuationDelivery(continuation,
|
|
539
|
+
async function persistA2AContinuationDelivery(continuation, history) {
|
|
414
540
|
const mapping = await getThreadMapping(continuation.platform, continuation.externalThreadId);
|
|
415
|
-
if (!mapping)
|
|
416
|
-
|
|
541
|
+
if (!mapping) {
|
|
542
|
+
throw new Error("Integration thread mapping is not available");
|
|
543
|
+
}
|
|
417
544
|
const thread = await getThread(mapping.internalThreadId);
|
|
418
|
-
if (!thread)
|
|
419
|
-
|
|
545
|
+
if (!thread) {
|
|
546
|
+
throw new Error("Integration chat thread is not available");
|
|
547
|
+
}
|
|
420
548
|
let repo;
|
|
421
549
|
try {
|
|
422
550
|
repo = JSON.parse(thread.threadData || "{}");
|
|
@@ -426,30 +554,31 @@ async function persistA2AContinuationDelivery(continuation, outgoing, receipt, a
|
|
|
426
554
|
}
|
|
427
555
|
if (!Array.isArray(repo.messages))
|
|
428
556
|
repo.messages = [];
|
|
429
|
-
const artifacts = extractA2AArtifactIdentities([{ tool: "call-agent", result: artifactText }], {
|
|
430
|
-
persistedArtifactSecrets: artifactSecrets,
|
|
431
|
-
});
|
|
432
557
|
const metadata = {
|
|
433
558
|
integrationDeliveryAttempted: true,
|
|
434
559
|
integrationDelivery: {
|
|
435
560
|
platform: continuation.platform,
|
|
436
561
|
status: "delivered",
|
|
437
|
-
text:
|
|
438
|
-
deliveredAt:
|
|
439
|
-
...(
|
|
440
|
-
? { messageRefs:
|
|
562
|
+
text: history.text,
|
|
563
|
+
deliveredAt: history.deliveredAt,
|
|
564
|
+
...(history.messageRefs.length
|
|
565
|
+
? { messageRefs: history.messageRefs }
|
|
441
566
|
: {}),
|
|
442
567
|
},
|
|
443
568
|
};
|
|
444
|
-
if (artifacts.length > 0)
|
|
445
|
-
metadata.integrationArtifacts = artifacts;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
569
|
+
if (history.artifacts.length > 0) {
|
|
570
|
+
metadata.integrationArtifacts = history.artifacts;
|
|
571
|
+
}
|
|
572
|
+
const messageId = `msg-${continuation.id}-assistant-continuation`;
|
|
573
|
+
if (!repo.messages.some((message) => message?.id === messageId)) {
|
|
574
|
+
repo.messages.push({
|
|
575
|
+
id: messageId,
|
|
576
|
+
role: "assistant",
|
|
577
|
+
content: [{ type: "text", text: history.text }],
|
|
578
|
+
createdAt: history.deliveredAt,
|
|
579
|
+
metadata,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
453
582
|
const meta = extractThreadMeta(repo);
|
|
454
583
|
await updateThreadData(mapping.internalThreadId, JSON.stringify(repo), meta.title || thread.title || "Integration Chat", meta.preview || thread.preview || "", repo.messages.length);
|
|
455
584
|
}
|
|
@@ -459,31 +588,100 @@ async function rescheduleAndRedispatchA2AContinuation(continuationId) {
|
|
|
459
588
|
console.error(`[integrations] Failed to redispatch A2A continuation ${continuationId}:`, err);
|
|
460
589
|
});
|
|
461
590
|
}
|
|
462
|
-
async function
|
|
463
|
-
|
|
591
|
+
async function recordTerminalA2ADelivery(continuation, kind, outgoing, receipt, artifacts, errorMessage) {
|
|
592
|
+
const historyPayload = {
|
|
593
|
+
text: outgoing.text,
|
|
594
|
+
deliveredAt: new Date().toISOString(),
|
|
595
|
+
messageRefs: receipt.messageRefs ?? [],
|
|
596
|
+
artifacts,
|
|
597
|
+
};
|
|
464
598
|
for (let attempt = 0; attempt < COMPLETE_AFTER_DELIVERY_ATTEMPTS; attempt++) {
|
|
465
599
|
try {
|
|
466
|
-
await
|
|
467
|
-
|
|
600
|
+
const confirmed = await recordA2ATerminalDeliveryReceipt(continuation.id, kind, historyPayload, errorMessage);
|
|
601
|
+
logA2AContinuationTransition(kind === "success"
|
|
602
|
+
? "response_delivery_confirmed"
|
|
603
|
+
: "failure_delivery_confirmed", continuation);
|
|
604
|
+
return confirmed;
|
|
605
|
+
}
|
|
606
|
+
catch { }
|
|
607
|
+
}
|
|
608
|
+
console.error(`[integrations] ${continuation.platform} accepted terminal A2A delivery for ${continuation.id}, ` +
|
|
609
|
+
`but recording its receipt failed after ${COMPLETE_AFTER_DELIVERY_ATTEMPTS} attempts. Leaving it in delivering for stale recovery.`);
|
|
610
|
+
try {
|
|
611
|
+
await retainA2AUnconfirmedDeliveryClaim(continuation.id);
|
|
612
|
+
}
|
|
613
|
+
catch (err) {
|
|
614
|
+
console.error(`[integrations] Failed to retain unconfirmed A2A delivery claim ${continuation.id}:`, err instanceof Error ? err.name : "receipt_recovery_error");
|
|
615
|
+
}
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
async function persistAndFinalizeConfirmedA2ADelivery(continuation) {
|
|
619
|
+
const history = continuation.terminalHistoryPayload;
|
|
620
|
+
if (!history || continuation.terminalDeliveryConfirmedAt == null) {
|
|
621
|
+
throw new Error("Confirmed A2A delivery is missing durable history");
|
|
622
|
+
}
|
|
623
|
+
let lastError;
|
|
624
|
+
let historyFinalized = false;
|
|
625
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
626
|
+
try {
|
|
627
|
+
if (!historyFinalized) {
|
|
628
|
+
await persistA2AContinuationDelivery(continuation, history);
|
|
629
|
+
await finalizeA2ATerminalHistory(continuation.id);
|
|
630
|
+
historyFinalized = true;
|
|
631
|
+
logA2AContinuationTransition("terminal_history_persisted", continuation);
|
|
632
|
+
}
|
|
633
|
+
await completeParentCampaignAfterTerminalA2A({
|
|
634
|
+
...continuation,
|
|
635
|
+
status: continuation.terminalDeliveryKind === "success"
|
|
636
|
+
? "completed"
|
|
637
|
+
: "failed",
|
|
638
|
+
});
|
|
468
639
|
return;
|
|
469
640
|
}
|
|
470
641
|
catch (err) {
|
|
471
642
|
lastError = err;
|
|
472
643
|
}
|
|
473
644
|
}
|
|
474
|
-
console.error(
|
|
475
|
-
|
|
645
|
+
console.error(historyFinalized
|
|
646
|
+
? `[integrations] A2A continuation ${continuation.id} finalized history but parent completion remains retryable:`
|
|
647
|
+
: `[integrations] A2A continuation ${continuation.id} has a provider receipt but its history remains retryable:`, lastError instanceof Error ? lastError.name : "persistence_error");
|
|
648
|
+
if (historyFinalized) {
|
|
649
|
+
await dispatchPendingIntegrationTask({
|
|
650
|
+
taskId: continuation.integrationTaskId,
|
|
651
|
+
task: {
|
|
652
|
+
platform: continuation.platform,
|
|
653
|
+
externalThreadId: continuation.externalThreadId,
|
|
654
|
+
platformContext: continuation.incoming.platformContext,
|
|
655
|
+
},
|
|
656
|
+
campaignContinuation: true,
|
|
657
|
+
allowPortableConfirmedReceiptReconciliation: true,
|
|
658
|
+
}).catch((err) => {
|
|
659
|
+
console.error(`[integrations] Failed to wake A2A parent ${continuation.integrationTaskId} after terminal history finalization:`, err);
|
|
660
|
+
});
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
await rescheduleAndRedispatchA2AContinuation(continuation.id);
|
|
476
664
|
}
|
|
477
665
|
async function completeParentCampaignAfterTerminalA2A(continuation) {
|
|
478
666
|
const campaign = await getIntegrationCampaignForTask(continuation.integrationTaskId);
|
|
479
667
|
if (!campaign)
|
|
480
668
|
return;
|
|
481
|
-
|
|
669
|
+
const outcome = await getA2AContinuationTaskOutcome(continuation.integrationTaskId);
|
|
670
|
+
if (outcome !== "terminal-delivered") {
|
|
671
|
+
if (outcome === "terminal-without-delivery") {
|
|
672
|
+
if (await reconcileTerminalA2AParentIfDisabled(continuation.integrationTaskId)) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (outcome !== "active") {
|
|
677
|
+
console.warn(`[integrations] Refusing to complete A2A parent ${continuation.integrationTaskId} with outcome ${outcome}`);
|
|
678
|
+
}
|
|
482
679
|
return;
|
|
483
680
|
}
|
|
484
681
|
const completed = await completeIntegrationCampaignTaskAfterA2A(continuation.integrationTaskId);
|
|
485
682
|
if (!completed)
|
|
486
683
|
return;
|
|
684
|
+
logA2AContinuationTransition("parent_completed", continuation);
|
|
487
685
|
const nextTask = await getNextPendingTaskForThread(continuation.platform, continuation.externalThreadId);
|
|
488
686
|
if (!nextTask)
|
|
489
687
|
return;
|
|
@@ -550,21 +748,37 @@ function remotePollFailureReason(continuation) {
|
|
|
550
748
|
function sleep(ms) {
|
|
551
749
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
552
750
|
}
|
|
553
|
-
async function withTimeout(
|
|
751
|
+
async function withTimeout(operation, timeoutMs, message) {
|
|
752
|
+
const controller = new AbortController();
|
|
554
753
|
let timer;
|
|
754
|
+
let timedOut = false;
|
|
555
755
|
try {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
new
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
756
|
+
timer = setTimeout(() => {
|
|
757
|
+
timedOut = true;
|
|
758
|
+
controller.abort(new Error(message));
|
|
759
|
+
}, timeoutMs);
|
|
760
|
+
const result = await operation(controller.signal);
|
|
761
|
+
if (timedOut)
|
|
762
|
+
throw new Error(message);
|
|
763
|
+
return result;
|
|
764
|
+
}
|
|
765
|
+
catch (error) {
|
|
766
|
+
if (timedOut)
|
|
767
|
+
throw new Error(message);
|
|
768
|
+
throw error;
|
|
562
769
|
}
|
|
563
770
|
finally {
|
|
564
771
|
if (timer)
|
|
565
772
|
clearTimeout(timer);
|
|
566
773
|
}
|
|
567
774
|
}
|
|
775
|
+
function throwIfAborted(signal) {
|
|
776
|
+
if (!signal.aborted)
|
|
777
|
+
return;
|
|
778
|
+
throw signal.reason instanceof Error
|
|
779
|
+
? signal.reason
|
|
780
|
+
: new Error("Platform delivery was aborted");
|
|
781
|
+
}
|
|
568
782
|
function sanitizeFailureReason(reason) {
|
|
569
783
|
const oneLine = reason.replace(/\s+/g, " ").trim();
|
|
570
784
|
const withoutEnvNames = oneLine.replace(/\b[A-Z][A-Z0-9_]*(?:API_KEY|PRIVATE_KEY|SECRET|TOKEN)\b/g, "a required credential");
|