@agent-native/core 0.186.0 → 0.187.0-nightly-20260923203303

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 (44) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/app/components/library/page-header.tsx +5 -37
  3. package/corpus/templates/clips/app/i18n/en-US.ts +0 -1
  4. package/corpus/templates/mail/AGENTS.md +1 -1
  5. package/corpus/templates/mail/actions/get-ai-priority.ts +26 -9
  6. package/corpus/templates/mail/actions/get-jev-availability.ts +23 -0
  7. package/corpus/templates/mail/actions/navigate.ts +19 -2
  8. package/corpus/templates/mail/app/components/email/EmailList.tsx +13 -4
  9. package/corpus/templates/mail/app/pages/InboxPage.tsx +47 -6
  10. package/corpus/templates/mail/changelog/2026-09-22-jev-powered-mail-rules-and-priority-sort-work-with-an-enable.md +6 -0
  11. package/corpus/templates/mail/changelog/2026-09-23-preserve-priority-sort-on-availability-error.md +6 -0
  12. package/corpus/templates/mail/server/lib/automation-engine.ts +147 -132
  13. package/corpus/templates/mail/server/lib/automation-model.ts +32 -1
  14. package/dist/agent/jev-tool-prefetch.d.ts +27 -3
  15. package/dist/agent/jev-tool-prefetch.js +103 -23
  16. package/dist/agent/production-agent.d.ts +9 -0
  17. package/dist/agent/production-agent.js +55 -23
  18. package/dist/app-config/integrations.d.ts +1 -0
  19. package/dist/app-config/integrations.js +7 -0
  20. package/dist/app-config/schema.d.ts +1 -0
  21. package/dist/automation/index.d.ts +1 -1
  22. package/dist/integrations/a2a-continuation-processor.js +35 -3
  23. package/dist/integrations/a2a-continuations-store.d.ts +2 -0
  24. package/dist/integrations/a2a-continuations-store.js +44 -2
  25. package/dist/integrations/integration-campaign-recovery.js +13 -2
  26. package/dist/integrations/integration-campaigns-store.d.ts +1 -0
  27. package/dist/integrations/integration-campaigns-store.js +18 -0
  28. package/dist/integrations/integration-durable-dispatch-config.js +2 -8
  29. package/dist/integrations/integration-durable-dispatch.d.ts +2 -0
  30. package/dist/integrations/integration-durable-dispatch.js +20 -4
  31. package/dist/integrations/plugin.js +11 -1
  32. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  33. package/dist/notifications/routes.d.ts +3 -3
  34. package/dist/resources/handlers.d.ts +1 -1
  35. package/dist/secrets/routes.d.ts +6 -6
  36. package/dist/server/agent-chat/prompt-resources.d.ts +1 -0
  37. package/dist/server/agent-chat/prompt-resources.js +5 -1
  38. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  39. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  40. package/dist/server/credential-provider.d.ts +1 -1
  41. package/dist/server/credential-provider.js +4 -3
  42. package/dist/server/index.d.ts +2 -1
  43. package/dist/server/index.js +2 -1
  44. package/package.json +3 -3
@@ -1,3 +1,4 @@
1
+ import { getBuilderProxyOrigin, } from "../server/credential-provider.js";
1
2
  import { getBuilderGatewayRequestHeaders } from "./engine/builder-gateway-headers.js";
2
3
  import { searchToolRegistry, TOOL_SEARCH_ACTION_NAME } from "./tool-search.js";
3
4
  const MAX_JEV_CANDIDATES = 128;
@@ -5,7 +6,6 @@ const DEFAULT_PREFETCH_LIMIT = 3;
5
6
  const MAX_PREFETCH_LIMIT = 5;
6
7
  const JEV_TIMEOUT_MS = 750;
7
8
  const JEV_MODEL = "jev-latest";
8
- export const BUILDER_JEV_PROXY_ENABLED = true;
9
9
  /**
10
10
  * Rank a bounded metadata-only catalog with Jev. A missing key, malformed
11
11
  * response, timeout, or provider failure returns no ranking so callers keep
@@ -13,8 +13,8 @@ export const BUILDER_JEV_PROXY_ENABLED = true;
13
13
  */
14
14
  export async function rankJevCandidates(options) {
15
15
  const request = options.request.trim();
16
- const apiKey = options.apiKey?.trim();
17
- const builderAuth = BUILDER_JEV_PROXY_ENABLED ? options.builderAuth : null;
16
+ const apiKey = options.personalApiKey?.trim();
17
+ const builderAuth = options.builderAuth;
18
18
  if (!request ||
19
19
  (!apiKey && !builderAuth) ||
20
20
  options.candidates.length === 0) {
@@ -51,6 +51,7 @@ export async function rankJevCandidates(options) {
51
51
  };
52
52
  const response = await requestJev({
53
53
  apiKey,
54
+ personalApiKey: options.personalApiKey,
54
55
  builderAuth,
55
56
  request: jevRequest,
56
57
  });
@@ -111,8 +112,8 @@ export function shortlistJevCandidates(request, candidates) {
111
112
  */
112
113
  export async function preloadJevTools(options) {
113
114
  const request = options.request.trim();
114
- const apiKey = options.apiKey?.trim();
115
- const builderAuth = BUILDER_JEV_PROXY_ENABLED ? options.builderAuth : null;
115
+ const apiKey = options.personalApiKey?.trim();
116
+ const builderAuth = options.builderAuth;
116
117
  if (!request || (!apiKey && !builderAuth)) {
117
118
  return options.initialTools;
118
119
  }
@@ -142,6 +143,7 @@ export async function preloadJevTools(options) {
142
143
  const selectedNames = await rankJevCandidates({
143
144
  request,
144
145
  apiKey,
146
+ personalApiKey: options.personalApiKey,
145
147
  candidates: candidates.map((candidate) => ({
146
148
  id: candidate.name,
147
149
  description: candidate.description,
@@ -158,63 +160,141 @@ export async function preloadJevTools(options) {
158
160
  : options.initialTools;
159
161
  }
160
162
  async function requestJev(options) {
161
- if (BUILDER_JEV_PROXY_ENABLED && options.builderAuth) {
163
+ const personalApiKey = options.personalApiKey?.trim();
164
+ if (options.builderAuth) {
162
165
  try {
163
166
  return await requestJevThroughBuilder(options.builderAuth, options.request);
164
167
  }
165
168
  catch (error) {
166
- if (!options.apiKey)
169
+ if (!personalApiKey)
167
170
  throw error;
168
171
  console.warn("[agent] Builder Jev proxy unavailable; falling back to the direct Jev API.", error instanceof Error ? error.message : "unknown error");
172
+ return requestJevDirect(personalApiKey, options.request);
169
173
  }
170
174
  }
171
- if (!options.apiKey) {
175
+ if (!personalApiKey) {
172
176
  throw new Error("Builder Jev proxy is unavailable.");
173
177
  }
178
+ return requestJevDirect(personalApiKey, options.request);
179
+ }
180
+ async function requestJevDirect(apiKey, request) {
174
181
  const { choice, TypeSafeClient } = await import("@typesafe-ai/sdk");
175
182
  const client = new TypeSafeClient({
176
- apiKey: options.apiKey,
183
+ apiKey,
177
184
  timeout: JEV_TIMEOUT_MS,
178
185
  retry: { maxRetries: 0 },
179
186
  });
180
187
  const systemOne = client.systemOne.bind(client);
181
188
  return (await systemOne({
182
- ...options.request,
189
+ ...request,
183
190
  questions: {
184
- [Object.keys(options.request.questions)[0]]: choice(Object.values(options.request.questions)[0].instructions, Object.values(options.request.questions)[0].criteria),
191
+ [Object.keys(request.questions)[0]]: choice(Object.values(request.questions)[0].instructions, Object.values(request.questions)[0].criteria),
185
192
  },
186
193
  }));
187
194
  }
188
- async function requestJevThroughBuilder(auth, request) {
189
- const { getBuilderProxyOrigin } = await import("../server/credential-provider.js");
195
+ export async function requestJevThroughBuilder(auth, request, options = {}) {
190
196
  const controller = new AbortController();
191
- const timeout = setTimeout(() => controller.abort(), JEV_TIMEOUT_MS);
197
+ const signal = options.signal
198
+ ? AbortSignal.any([options.signal, controller.signal])
199
+ : controller.signal;
200
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? JEV_TIMEOUT_MS);
192
201
  try {
193
- const response = await fetch(`${getBuilderProxyOrigin().replace(/\/+$/, "")}/agent-native/jev/v1/system-one`, {
194
- method: "POST",
202
+ for (let attempt = 0; attempt < 2; attempt += 1) {
203
+ signal.throwIfAborted();
204
+ const response = await fetch(`${getBuilderProxyOrigin().replace(/\/+$/, "")}/agent-native/jev/v1/system-one`, {
205
+ method: "POST",
206
+ headers: {
207
+ "Content-Type": "application/json",
208
+ Authorization: auth.authorization,
209
+ ...(auth.spaceId ? { "x-builder-api-key": auth.spaceId } : {}),
210
+ ...(auth.userId ? { "x-builder-user-id": auth.userId } : {}),
211
+ ...getBuilderGatewayRequestHeaders(),
212
+ },
213
+ body: JSON.stringify(request),
214
+ signal,
215
+ });
216
+ if (!response.ok) {
217
+ if (attempt === 0 && [429, 529].includes(response.status)) {
218
+ await response.body?.cancel().catch(() => undefined);
219
+ await waitForJevRetry(signal);
220
+ continue;
221
+ }
222
+ throw new Error(`Builder Jev proxy returned HTTP ${response.status}.`);
223
+ }
224
+ const result = (await response.json());
225
+ if (!isJevResponse(result)) {
226
+ throw new Error("Builder Jev proxy returned an invalid response.");
227
+ }
228
+ return result;
229
+ }
230
+ throw new Error("Builder Jev proxy request failed.");
231
+ }
232
+ finally {
233
+ clearTimeout(timeout);
234
+ }
235
+ }
236
+ function waitForJevRetry(signal) {
237
+ return new Promise((resolve, reject) => {
238
+ const onAbort = () => {
239
+ clearTimeout(timer);
240
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
241
+ };
242
+ const timer = setTimeout(() => {
243
+ signal.removeEventListener("abort", onAbort);
244
+ resolve();
245
+ }, 200);
246
+ signal.addEventListener("abort", onAbort, { once: true });
247
+ if (signal.aborted)
248
+ onAbort();
249
+ });
250
+ }
251
+ export async function isBuilderJevEnabled(auth, options = {}) {
252
+ const controller = new AbortController();
253
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 3_000);
254
+ try {
255
+ const response = await fetch(`${getBuilderProxyOrigin().replace(/\/+$/, "")}/agent-native/jev/v1/status`, {
256
+ method: "GET",
195
257
  headers: {
196
- "Content-Type": "application/json",
197
258
  Authorization: auth.authorization,
198
259
  ...(auth.spaceId ? { "x-builder-api-key": auth.spaceId } : {}),
199
260
  ...(auth.userId ? { "x-builder-user-id": auth.userId } : {}),
200
261
  ...getBuilderGatewayRequestHeaders(),
201
262
  },
202
- body: JSON.stringify(request),
203
- signal: controller.signal,
263
+ signal: options.signal
264
+ ? AbortSignal.any([options.signal, controller.signal])
265
+ : controller.signal,
204
266
  });
267
+ if (response.status === 403)
268
+ return false;
205
269
  if (!response.ok) {
206
- throw new Error(`Builder Jev proxy returned HTTP ${response.status}.`);
270
+ throw new Error(`Builder Jev status returned HTTP ${response.status}.`);
207
271
  }
208
272
  const result = (await response.json());
209
- if (!isJevResponse(result)) {
210
- throw new Error("Builder Jev proxy returned an invalid response.");
273
+ if (typeof result !== "object" ||
274
+ result === null ||
275
+ !("enabled" in result) ||
276
+ typeof result.enabled !== "boolean") {
277
+ throw new Error("Builder Jev status returned an invalid response.");
211
278
  }
212
- return result;
279
+ return result.enabled;
213
280
  }
214
281
  finally {
215
282
  clearTimeout(timeout);
216
283
  }
217
284
  }
285
+ export async function isJevEnabled(credentials) {
286
+ if (credentials.personalApiKey)
287
+ return true;
288
+ if (credentials.builderAuth) {
289
+ const enabled = await isBuilderJevEnabled(credentials.builderAuth);
290
+ if (enabled || !credentials.apiKeyLookupFailed)
291
+ return enabled;
292
+ }
293
+ if (credentials.apiKeyLookupFailed || credentials.builderAuthLookupFailed) {
294
+ throw new Error("Could not check Jev credentials or Builder entitlement.");
295
+ }
296
+ return false;
297
+ }
218
298
  function isJevResponse(value) {
219
299
  return (typeof value === "object" &&
220
300
  value !== null &&
@@ -4,6 +4,7 @@ import type { Task } from "../a2a/types.js";
4
4
  import { type ActionAutomationContext, type ActionCaller } from "../action.js";
5
5
  import { type ArtifactReceipt } from "../artifacts/detect.js";
6
6
  import type { AgentNativeHarnessSetting } from "../config.js";
7
+ import { type BuilderGatewayAuth } from "../server/credential-provider.js";
7
8
  import { fireInternalDispatch } from "../server/self-dispatch.js";
8
9
  import { type ReasoningEffort } from "../shared/reasoning-effort.js";
9
10
  import { type ResolveEngineConfig } from "./engine/index.js";
@@ -115,6 +116,14 @@ export declare function getOwnerApiKey(provider: string, ownerEmail: string | nu
115
116
  * staleness without adding a secret-store read to every request.
116
117
  */
117
118
  export declare function getOwnerJevApiKey(ownerEmail: string | null | undefined): Promise<string | undefined>;
119
+ export interface JevContextCredentials {
120
+ apiKey: string | undefined;
121
+ personalApiKey: string | undefined;
122
+ builderAuth: BuilderGatewayAuth | null;
123
+ apiKeyLookupFailed?: boolean;
124
+ builderAuthLookupFailed?: boolean;
125
+ }
126
+ export declare function getJevContextCredentials(ownerEmail: string | null | undefined): Promise<JevContextCredentials>;
118
127
  /**
119
128
  * Derive the provider name from the active engine setting.
120
129
  * "ai-sdk:openai" → "openai", "anthropic" → "anthropic"
@@ -47,7 +47,7 @@ import { loadPriorTurnToolCallJournal } from "./engine/tool-call-journal-seed.js
47
47
  import { backfillEngineMessagesToolResults, stringifyToolUseInputForGateway, unmatchedToolResultReplayText, } from "./engine/translate-anthropic.js";
48
48
  import { EngineError } from "./engine/types.js";
49
49
  import { filterHostedHarnessToolNames, normalizeHostedHarnessRuntime, } from "./harness/hosted.js";
50
- import { BUILDER_JEV_PROXY_ENABLED, preloadJevTools, } from "./jev-tool-prefetch.js";
50
+ import { preloadJevTools } from "./jev-tool-prefetch.js";
51
51
  import { getDefaultMaxIterations, getDefaultMaxRunInputTokens, MAX_AGENT_MAX_ITERATIONS, MIN_AGENT_MAX_ITERATIONS, normalizeMaxIterations, normalizeMaxRunInputTokens, readAgentLoopSettings, } from "./loop-settings.js";
52
52
  import { getContextWindowForModel, resolveFallbackModel, } from "./model-config.js";
53
53
  import { maybeCompactThread, buildObservationalContext, hasObservationalMemory, serializeObservationalMemoryBlock, } from "./observational-memory/index.js";
@@ -375,17 +375,31 @@ export async function getOwnerApiKey(provider, ownerEmail, options) {
375
375
  * staleness without adding a secret-store read to every request.
376
376
  */
377
377
  export async function getOwnerJevApiKey(ownerEmail) {
378
+ return (await getOwnerJevApiKeyCredential(ownerEmail)).credential?.apiKey;
379
+ }
380
+ async function getOwnerJevApiKeyCredential(ownerEmail) {
378
381
  if (!ownerEmail)
379
- return undefined;
382
+ return { credential: null, lookupFailed: false };
380
383
  const cacheKey = [
381
- "jev",
384
+ "jev-source-v1",
382
385
  ownerEmail,
383
386
  getRequestOrgId() ?? `solo:${ownerEmail}`,
384
387
  getRequestContext()?.isSyntheticTraffic === true ? "synthetic" : "normal",
385
388
  ].join("\u0000");
386
389
  const cached = readOptionalKeyCache(cacheKey);
387
- if (cached.hit)
388
- return cached.value;
390
+ if (cached.hit) {
391
+ if (!cached.value)
392
+ return { credential: null, lookupFailed: false };
393
+ const separator = cached.value.indexOf(":");
394
+ const source = cached.value.slice(0, separator);
395
+ const apiKey = cached.value.slice(separator + 1);
396
+ return {
397
+ credential: (source === "user" || source === "deployment") && apiKey
398
+ ? { apiKey, source }
399
+ : null,
400
+ lookupFailed: false,
401
+ };
402
+ }
389
403
  let lookupFailed = false;
390
404
  const value = await getOwnerApiKey("jev", ownerEmail, {
391
405
  onLookupFailure: () => {
@@ -394,11 +408,14 @@ export async function getOwnerJevApiKey(ownerEmail) {
394
408
  });
395
409
  if (value) {
396
410
  if (!lookupFailed)
397
- writeOptionalKeyCache(cacheKey, value);
398
- return value;
411
+ writeOptionalKeyCache(cacheKey, `user:${value}`);
412
+ return {
413
+ credential: { apiKey: value, source: "user" },
414
+ lookupFailed,
415
+ };
399
416
  }
400
417
  if (lookupFailed)
401
- return undefined;
418
+ return { credential: null, lookupFailed: true };
402
419
  const deployKey = canUseDeployCredentialFallbackForRequest("JEV_API_KEY")
403
420
  ? readDeployCredentialEnv("JEV_API_KEY")?.trim()
404
421
  : undefined;
@@ -407,24 +424,37 @@ export async function getOwnerJevApiKey(ownerEmail) {
407
424
  key: "JEV_API_KEY",
408
425
  value: deployKey,
409
426
  }))) {
410
- if (!lookupFailed)
411
- writeOptionalKeyCache(cacheKey, deployKey);
412
- return deployKey;
427
+ if (!lookupFailed) {
428
+ writeOptionalKeyCache(cacheKey, `deployment:${deployKey}`);
429
+ }
430
+ return {
431
+ credential: { apiKey: deployKey, source: "deployment" },
432
+ lookupFailed: false,
433
+ };
413
434
  }
414
435
  if (!lookupFailed)
415
436
  writeOptionalKeyCache(cacheKey, undefined);
416
- return undefined;
437
+ return { credential: null, lookupFailed: false };
417
438
  }
418
- async function getJevContextCredentials(ownerEmail) {
419
- const apiKey = await getOwnerJevApiKey(ownerEmail);
420
- if (!BUILDER_JEV_PROXY_ENABLED)
421
- return { apiKey, builderAuth: null };
422
- try {
423
- return { apiKey, builderAuth: await resolveBuilderGatewayAuth() };
424
- }
425
- catch {
426
- return { apiKey, builderAuth: null };
427
- }
439
+ export async function getJevContextCredentials(ownerEmail) {
440
+ const requestContext = getRequestContext();
441
+ const [lookup, builderAuthLookup] = await Promise.all([
442
+ getOwnerJevApiKeyCredential(ownerEmail),
443
+ resolveBuilderGatewayAuth({
444
+ userEmail: ownerEmail,
445
+ orgId: requestContext?.orgScope === "personal" ? null : getRequestOrgId(),
446
+ }).then((builderAuth) => ({ builderAuth, lookupFailed: false }), () => ({ builderAuth: null, lookupFailed: true })),
447
+ ]);
448
+ const credential = lookup.credential;
449
+ return {
450
+ apiKey: credential?.apiKey,
451
+ personalApiKey: credential?.source === "user" ? credential.apiKey : undefined,
452
+ builderAuth: builderAuthLookup.builderAuth,
453
+ ...(lookup.lookupFailed ? { apiKeyLookupFailed: true } : {}),
454
+ ...(builderAuthLookup.lookupFailed
455
+ ? { builderAuthLookupFailed: true }
456
+ : {}),
457
+ };
428
458
  }
429
459
  /**
430
460
  * Derive the provider name from the active engine setting.
@@ -8040,7 +8070,7 @@ export function createProductionAgentHandler(options) {
8040
8070
  presendCap("files", filesContextThunk, "", 12000),
8041
8071
  presendCap("loopSettings", loopSettingsThunk, fallbackLoopSettings, 9000),
8042
8072
  presendCap("enrichedMessage", enrichedMessageThunk, requestMessage, 9000),
8043
- presendCap("jevContextCredentials", () => getJevContextCredentials(ownerEmail ?? getRequestUserEmail()), { apiKey: undefined, builderAuth: null }, 9000),
8073
+ presendCap("jevContextCredentials", () => getJevContextCredentials(ownerEmail ?? getRequestUserEmail()), { apiKey: undefined, personalApiKey: undefined, builderAuth: null }, 9000),
8044
8074
  ]);
8045
8075
  setupMark("ctxAll");
8046
8076
  // DIAGNOSTIC-ONLY: all parallel context gathering (system prompt, screen,
@@ -8086,6 +8116,7 @@ export function createProductionAgentHandler(options) {
8086
8116
  preloadJevTools({
8087
8117
  request: requestMessage,
8088
8118
  apiKey: jevContextCredentials.apiKey,
8119
+ personalApiKey: jevContextCredentials.personalApiKey,
8089
8120
  builderAuth: jevContextCredentials.builderAuth,
8090
8121
  registry: requestActions,
8091
8122
  initialTools: curatedRequestTools,
@@ -8095,6 +8126,7 @@ export function createProductionAgentHandler(options) {
8095
8126
  preloadJevContextForPrompt({
8096
8127
  request: requestMessage,
8097
8128
  apiKey: jevContextCredentials.apiKey,
8129
+ personalApiKey: jevContextCredentials.personalApiKey,
8098
8130
  builderAuth: jevContextCredentials.builderAuth,
8099
8131
  compact: options.jevContextCompact,
8100
8132
  maxChars: jevContextMaxChars,
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  /** Inbound integration webhook policy. */
3
3
  export declare const integrationsConfig: z.ZodObject<{
4
+ durableDispatch: z.ZodOptional<z.ZodBoolean>;
4
5
  allowUnverifiedWebhooks: z.ZodDefault<z.ZodBoolean>;
5
6
  webhookBaseUrl: z.ZodOptional<z.ZodString>;
6
7
  platforms: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -1,6 +1,13 @@
1
1
  import { z } from "zod";
2
2
  /** Inbound integration webhook policy. */
3
3
  export const integrationsConfig = z.object({
4
+ durableDispatch: z
5
+ .boolean()
6
+ .optional()
7
+ .meta({
8
+ env: ["AGENT_INTEGRATION_DURABLE_DISPATCH"],
9
+ doc: "Use a durable background function for integration work. Explicit false stops an enabled rollout; unset leaves it unavailable.",
10
+ }),
4
11
  allowUnverifiedWebhooks: z
5
12
  .boolean()
6
13
  .default(false)
@@ -76,6 +76,7 @@ export declare const appConfigSchema: z.ZodObject<{
76
76
  requireEmailVerification: z.ZodOptional<z.ZodBoolean>;
77
77
  }, z.core.$strip>>;
78
78
  integrations: z.ZodPrefault<z.ZodObject<{
79
+ durableDispatch: z.ZodOptional<z.ZodBoolean>;
79
80
  allowUnverifiedWebhooks: z.ZodDefault<z.ZodBoolean>;
80
81
  webhookBaseUrl: z.ZodOptional<z.ZodString>;
81
82
  platforms: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -186,8 +186,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
186
186
  duplicate?: undefined;
187
187
  eventId?: undefined;
188
188
  } | {
189
+ error?: undefined;
189
190
  accepted: boolean;
190
191
  duplicate: boolean;
191
192
  eventId: string;
192
- error?: undefined;
193
193
  }>;
@@ -7,9 +7,9 @@ import { resolveArtifactBaseUrl } from "../server/agent-chat/action-filters-a2a.
7
7
  import { withConfiguredAppBasePath } from "../server/app-base-path.js";
8
8
  import { FRAMEWORK_ROUTE_PREFIX } from "../server/core-routes-plugin.js";
9
9
  import { resolveSelfDispatchBaseUrl } from "../server/self-dispatch.js";
10
- import { claimA2AContinuation, claimA2AContinuationDelivery, claimDueA2AContinuations, failA2AContinuationsForIntegrationTask, failA2AContinuation, finalizeA2ATerminalHistory, getA2AContinuation, getA2AContinuationTaskOutcome, hasOnlyLegacyFailedA2AContinuationsForIntegrationTask, hasPendingConfirmedA2ADeliveryForIntegrationTask, listRecoverableA2AIntegrationTasks, recoverDueA2AContinuationIds, recordA2ATerminalDeliveryReceipt, retainA2AUnconfirmedDeliveryClaim, rescheduleA2AContinuation, saveA2AVerifiedArtifactCheckpoint, } from "./a2a-continuations-store.js";
10
+ import { claimA2AContinuation, claimA2AContinuationDelivery, claimDueA2AContinuations, failA2AContinuationsForIntegrationTask, failA2AContinuation, finalizeA2ATerminalHistory, getA2AContinuation, getA2AContinuationTaskOutcome, hasOnlyLegacyFailedA2AContinuationsForIntegrationTask, hasPendingConfirmedA2ADeliveryForIntegrationTask, listRecoverableA2AIntegrationTasks, deferA2AContinuationsForRuntime, recoverDueA2AContinuationIds, recordA2ATerminalDeliveryReceipt, retainA2AUnconfirmedDeliveryClaim, rescheduleA2AContinuation, pauseA2AContinuationForRuntime, saveA2AVerifiedArtifactCheckpoint, } from "./a2a-continuations-store.js";
11
11
  import { completeIntegrationCampaignTaskAfterA2A, failDisabledIntegrationCampaignTask, failIntegrationCampaignTaskDeliveryContainment, getIntegrationCampaignForTask, } from "./integration-campaigns-store.js";
12
- import { dispatchPendingIntegrationTask, isIntegrationDurableDispatchEnabledForTask, } from "./integration-durable-dispatch.js";
12
+ import { dispatchPendingIntegrationTask, integrationDurableDispatchRuntimeUnavailableReasons, isIntegrationDurableDispatchEnabledForTask, isIntegrationDurableDispatchExplicitlyDisabledForTask, } from "./integration-durable-dispatch.js";
13
13
  import { signInternalToken } from "./internal-token.js";
14
14
  import { getNextPendingTaskForThread, getPendingTask, } from "./pending-tasks-store.js";
15
15
  import { getThreadMapping } from "./thread-mapping-store.js";
@@ -186,6 +186,7 @@ export async function recoverDueA2AContinuations(options) {
186
186
  const candidateTasks = await listRecoverableA2AIntegrationTasks(200);
187
187
  const eligibleTaskIds = [];
188
188
  const confirmedHistoryTaskIds = [];
189
+ const unavailableTaskIds = [];
189
190
  for (const task of candidateTasks) {
190
191
  const enabled = isIntegrationDurableDispatchEnabledForTask({
191
192
  platform: task.platform,
@@ -200,12 +201,22 @@ export async function recoverDueA2AContinuations(options) {
200
201
  else if (task.hasPendingConfirmedDelivery) {
201
202
  confirmedHistoryTaskIds.push(task.id);
202
203
  }
203
- else {
204
+ else if (isIntegrationDurableDispatchExplicitlyDisabledForTask({
205
+ platform: task.platform,
206
+ externalThreadId: task.externalThreadId,
207
+ platformContext: task.dispatchScope
208
+ ? { channelId: task.dispatchScope }
209
+ : undefined,
210
+ })) {
204
211
  await failDisabledDurableA2ATask(task);
205
212
  }
213
+ else {
214
+ unavailableTaskIds.push(task.id);
215
+ }
206
216
  if (eligibleTaskIds.length + confirmedHistoryTaskIds.length >= limit)
207
217
  break;
208
218
  }
219
+ await deferA2AContinuationsForRuntime(unavailableTaskIds, 2 * 60_000);
209
220
  const ids = await recoverDueA2AContinuationIds(limit, eligibleTaskIds);
210
221
  const remaining = Math.max(0, limit - ids.length);
211
222
  const confirmedHistoryIds = remaining > 0 && confirmedHistoryTaskIds.length > 0
@@ -341,6 +352,18 @@ async function durableContinuationScopeStillEnabled(continuation) {
341
352
  });
342
353
  if (enabled)
343
354
  return true;
355
+ if (task?.status === "processing" &&
356
+ !isIntegrationDurableDispatchExplicitlyDisabledForTask({
357
+ platform: task.platform,
358
+ externalThreadId: task.externalThreadId,
359
+ platformContext: task.dispatchScope
360
+ ? { channelId: task.dispatchScope }
361
+ : undefined,
362
+ })) {
363
+ await pauseA2AContinuationForRuntime(continuation.id, continuation.attempts, RESCHEDULE_DELAY_MS);
364
+ console.warn(`[integrations] A2A continuation ${continuation.id} paused: durable dispatch runtime unavailable`, integrationDurableDispatchRuntimeUnavailableReasons());
365
+ return false;
366
+ }
344
367
  if (await hasPendingConfirmedA2ADeliveryForIntegrationTask(continuation.integrationTaskId)) {
345
368
  await failA2AContinuation(continuation.id, "Durable integration campaign was disabled before this continuation delivered");
346
369
  await reconcileTerminalA2AParentIfDisabled(continuation.integrationTaskId);
@@ -393,6 +416,15 @@ export async function reconcileTerminalA2AParentIfDisabled(integrationTaskId) {
393
416
  })) {
394
417
  return false;
395
418
  }
419
+ if (!isIntegrationDurableDispatchExplicitlyDisabledForTask({
420
+ platform: task.platform,
421
+ externalThreadId: task.externalThreadId,
422
+ platformContext: task.dispatchScope
423
+ ? { channelId: task.dispatchScope }
424
+ : undefined,
425
+ })) {
426
+ return false;
427
+ }
396
428
  await failDisabledDurableA2ATask({
397
429
  id: task.id,
398
430
  platform: task.platform,
@@ -86,8 +86,10 @@ export interface RecoverableA2AIntegrationTask {
86
86
  * can filter the canary in memory without an N+1 pending-task lookup loop.
87
87
  */
88
88
  export declare function listRecoverableA2AIntegrationTasks(limit?: number): Promise<RecoverableA2AIntegrationTask[]>;
89
+ export declare function deferA2AContinuationsForRuntime(integrationTaskIds: string[], delayMs: number): Promise<void>;
89
90
  export declare function claimA2AContinuationDelivery(id: string): Promise<A2AContinuation | null>;
90
91
  export declare function rescheduleA2AContinuation(id: string, delayMs: number): Promise<void>;
92
+ export declare function pauseA2AContinuationForRuntime(id: string, claimedAttempts: number, delayMs: number): Promise<boolean>;
91
93
  export declare function retainA2AUnconfirmedDeliveryClaim(id: string): Promise<void>;
92
94
  export declare function saveA2AVerifiedArtifactCheckpoint(id: string, checkpoint: string): Promise<string | null>;
93
95
  export declare function recordA2ATerminalDeliveryReceipt(id: string, kind: A2ATerminalDeliveryKind, historyPayload: A2ATerminalHistoryPayload, errorMessage?: string): Promise<A2AContinuation>;
@@ -544,7 +544,7 @@ export async function listRecoverableA2AIntegrationTasks(limit = 50) {
544
544
  await ensureTable();
545
545
  const now = Date.now();
546
546
  const { rows } = await getDbExec().execute({
547
- sql: `SELECT DISTINCT c.integration_task_id, t.platform,
547
+ sql: `SELECT c.integration_task_id, t.platform,
548
548
  t.external_thread_id, t.dispatch_scope, t.status,
549
549
  EXISTS (
550
550
  SELECT 1 FROM integration_a2a_continuations receipt
@@ -562,7 +562,10 @@ export async function listRecoverableA2AIntegrationTasks(limit = 50) {
562
562
  OR (c.status = 'delivering' AND
563
563
  ((c.terminal_delivery_confirmed_at IS NOT NULL AND c.next_check_at <= ?)
564
564
  OR c.updated_at <= ?)))
565
- ORDER BY c.integration_task_id ASC
565
+ GROUP BY c.integration_task_id, t.platform, t.external_thread_id,
566
+ t.dispatch_scope, t.status
567
+ ORDER BY has_pending_confirmed_delivery DESC,
568
+ MIN(c.next_check_at) ASC, c.integration_task_id ASC
566
569
  LIMIT ?`,
567
570
  args: [
568
571
  now,
@@ -583,6 +586,32 @@ export async function listRecoverableA2AIntegrationTasks(limit = 50) {
583
586
  Number(row.has_pending_confirmed_delivery ?? 0) === 1,
584
587
  }));
585
588
  }
589
+ export async function deferA2AContinuationsForRuntime(integrationTaskIds, delayMs) {
590
+ if (integrationTaskIds.length === 0)
591
+ return;
592
+ await ensureTable();
593
+ const now = Date.now();
594
+ const taskFilter = integrationTaskIds.map(() => "?").join(", ");
595
+ await getDbExec().execute({
596
+ sql: `UPDATE integration_a2a_continuations
597
+ SET next_check_at = ?, updated_at = ?
598
+ WHERE integration_task_id IN (${taskFilter})
599
+ AND terminal_delivery_confirmed_at IS NULL
600
+ AND ((status = 'pending' AND next_check_at <= ?)
601
+ OR (status = 'processing' AND
602
+ (updated_at <= ? OR next_check_at <= ?))
603
+ OR (status = 'delivering' AND updated_at <= ?))`,
604
+ args: [
605
+ now + delayMs,
606
+ now,
607
+ ...integrationTaskIds,
608
+ now,
609
+ now - PROCESSING_STUCK_AFTER_MS,
610
+ now - PROCESSING_NEXT_CHECK_STALE_AFTER_MS,
611
+ now - PROCESSING_STUCK_AFTER_MS,
612
+ ],
613
+ });
614
+ }
586
615
  export async function claimA2AContinuationDelivery(id) {
587
616
  await ensureTable();
588
617
  const client = getDbExec();
@@ -608,6 +637,19 @@ export async function rescheduleA2AContinuation(id, delayMs) {
608
637
  args: ["pending", now + delayMs, now, id],
609
638
  });
610
639
  }
640
+ export async function pauseA2AContinuationForRuntime(id, claimedAttempts, delayMs) {
641
+ await ensureTable();
642
+ const now = Date.now();
643
+ const result = await getDbExec().execute({
644
+ sql: `UPDATE integration_a2a_continuations
645
+ SET status = 'pending', attempts = attempts - 1,
646
+ next_check_at = ?, updated_at = ?
647
+ WHERE id = ? AND status = 'processing' AND attempts = ?
648
+ RETURNING id`,
649
+ args: [now + delayMs, now, id, claimedAttempts],
650
+ });
651
+ return (result.rows?.length ?? 0) > 0;
652
+ }
611
653
  export async function retainA2AUnconfirmedDeliveryClaim(id) {
612
654
  await ensureTable();
613
655
  const now = Date.now();
@@ -1,6 +1,6 @@
1
1
  import { getA2AContinuationTaskOutcome } from "./a2a-continuations-store.js";
2
- import { getIntegrationCampaign, failDisabledIntegrationCampaignTask, listDueIntegrationCampaignIds, } from "./integration-campaigns-store.js";
3
- import { dispatchPendingIntegrationTask, isIntegrationDurableDispatchEnabledForTask, } from "./integration-durable-dispatch.js";
2
+ import { getIntegrationCampaign, failDisabledIntegrationCampaignTask, listDueIntegrationCampaignIds, deferIntegrationCampaignForRuntime, } from "./integration-campaigns-store.js";
3
+ import { dispatchPendingIntegrationTask, isIntegrationDurableDispatchEnabledForTask, isIntegrationDurableDispatchExplicitlyDisabledForTask, } from "./integration-durable-dispatch.js";
4
4
  import { getNextPendingTaskForThread, getPendingTask, } from "./pending-tasks-store.js";
5
5
  function hasConfirmedDeliveryReceipt(payload) {
6
6
  try {
@@ -46,6 +46,17 @@ export async function recoverDueIntegrationCampaigns(options) {
46
46
  const confirmedReceipt = hasConfirmedDeliveryReceipt(task.payload) ||
47
47
  (await getA2AContinuationTaskOutcome(task.id)) === "terminal-delivered";
48
48
  if (!durableDispatchEnabled && !confirmedReceipt) {
49
+ if (!isIntegrationDurableDispatchExplicitlyDisabledForTask({
50
+ platform: task.platform,
51
+ externalThreadId: task.externalThreadId,
52
+ platformContext: task.dispatchScope
53
+ ? { channelId: task.dispatchScope }
54
+ : undefined,
55
+ })) {
56
+ await deferIntegrationCampaignForRuntime(campaign.id, 60_000);
57
+ result.skipped += 1;
58
+ continue;
59
+ }
49
60
  await failDisabledIntegrationCampaignTask(task.id);
50
61
  const nextTask = await getNextPendingTaskForThread(task.platform, task.externalThreadId);
51
62
  if (nextTask) {
@@ -116,5 +116,6 @@ export declare function refreshIntegrationCampaignTaskA2AReceiptRetry(integratio
116
116
  errorMessage: string;
117
117
  }): Promise<boolean>;
118
118
  export declare function completeIntegrationCampaignTaskAfterA2A(integrationTaskId: string): Promise<boolean>;
119
+ export declare function deferIntegrationCampaignForRuntime(id: string, delayMs: number): Promise<boolean>;
119
120
  export declare function listDueIntegrationCampaignIds(limit?: number): Promise<string[]>;
120
121
  export declare function hasActiveIntegrationCampaign(integrationTaskId: string): Promise<boolean>;
@@ -725,6 +725,24 @@ export async function completeIntegrationCampaignTaskAfterA2A(integrationTaskId)
725
725
  }
726
726
  throw new Error("Database does not support atomic A2A parent completion");
727
727
  }
728
+ export async function deferIntegrationCampaignForRuntime(id, delayMs) {
729
+ await ensureTable();
730
+ const now = Date.now();
731
+ const nextRunAt = now + delayMs;
732
+ const result = await getDbExec().execute({
733
+ sql: `UPDATE integration_campaigns
734
+ SET next_run_at = ?,
735
+ lease_expires_at = CASE WHEN status = 'processing' THEN ? ELSE lease_expires_at END,
736
+ updated_at = ?
737
+ WHERE id = ? AND (
738
+ (status IN ('pending', 'waiting') AND next_run_at <= ?)
739
+ OR (status = 'processing' AND lease_expires_at IS NOT NULL AND lease_expires_at <= ?)
740
+ )
741
+ RETURNING id`,
742
+ args: [nextRunAt, nextRunAt, now, id, now, now],
743
+ });
744
+ return (result.rows?.length ?? 0) > 0;
745
+ }
728
746
  export async function listDueIntegrationCampaignIds(limit = 25) {
729
747
  await ensureTable();
730
748
  const boundedLimit = Math.max(1, Math.min(Math.floor(limit), MAX_DUE_LIST_LIMIT));