@agent-native/core 0.84.20 → 0.84.21

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 (109) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +8 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/caller-auth.ts +37 -8
  5. package/corpus/core/src/a2a/client.ts +178 -67
  6. package/corpus/core/src/agent/production-agent.ts +19 -3
  7. package/corpus/core/src/agent/types.ts +1 -1
  8. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
  9. package/corpus/core/src/cli/recap.ts +43 -34
  10. package/corpus/core/src/client/PoweredByBadge.tsx +70 -17
  11. package/corpus/core/src/client/agent-chat-adapter.ts +5 -1
  12. package/corpus/core/src/client/i18n.tsx +10 -5
  13. package/corpus/core/src/client/sse-event-processor.ts +147 -5
  14. package/corpus/core/src/file-upload/builder.ts +35 -14
  15. package/corpus/core/src/file-upload/types.ts +3 -0
  16. package/corpus/core/src/integrations/a2a-continuation-processor.ts +49 -24
  17. package/corpus/core/src/mcp/builtin-tools.ts +3 -0
  18. package/corpus/core/src/mcp/org-directory.ts +66 -20
  19. package/corpus/core/src/scripts/call-agent.ts +7 -2
  20. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +161 -12
  21. package/corpus/templates/analytics/changelog/2026-07-01-session-replays-load-large-production-recordings-from-scoped.md +6 -0
  22. package/corpus/templates/calendar/app/pages/BookingPage.tsx +14 -8
  23. package/corpus/templates/calendar/changelog/2026-07-01-meeting-invite-pages-have-cleaner-branding-a-black-dark-mo.md +6 -0
  24. package/corpus/templates/clips/actions/create-recording.ts +7 -2
  25. package/corpus/templates/clips/actions/finalize-recording.ts +11 -0
  26. package/corpus/templates/clips/actions/get-recording-player-data.ts +4 -2
  27. package/corpus/templates/clips/actions/import-loom-recording.ts +1 -0
  28. package/corpus/templates/clips/actions/lib/loom-video.ts +16 -0
  29. package/corpus/templates/clips/app/components/player/scrubber.tsx +2 -2
  30. package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +4 -3
  31. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +39 -12
  32. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +65 -1
  33. package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +1 -22
  34. package/corpus/templates/clips/app/routes/record.tsx +48 -8
  35. package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-loom-imports-finish-sooner-by-skipping-the-.md +6 -0
  36. package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-playback-recover-more-reliably-from-storage.md +6 -0
  37. package/corpus/templates/clips/changelog/2026-07-01-desktop-recording-avoids-unsupported-browser-tab-capture.md +6 -0
  38. package/corpus/templates/clips/changelog/2026-07-01-embedded-player-progress-is-easier-to-see-on-dark-videos.md +6 -0
  39. package/corpus/templates/clips/changelog/2026-07-01-loom-imports-show-clear-guidance-when-loom-does-not-provide-a-downloadable-video.md +6 -0
  40. package/corpus/templates/clips/changelog/2026-07-01-preserve-in-flight-streaming-uploads-when-the-streaming-kill.md +6 -0
  41. package/corpus/templates/clips/changelog/2026-07-01-recording-controls-are-cleaner-and-the-countdown-uses-shadow.md +6 -0
  42. package/corpus/templates/clips/changelog/2026-07-01-recordings-now-recover-when-final-upload-status-gets-lost.md +6 -0
  43. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  44. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +7 -224
  45. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +64 -6
  46. package/corpus/templates/clips/desktop/src/lib/recorder.ts +87 -5
  47. package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +7 -0
  48. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +14 -1
  49. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +4 -1
  50. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +96 -1
  51. package/corpus/templates/clips/shared/finalize-recovery.ts +242 -0
  52. package/corpus/templates/design/changelog/2026-07-01-design-recovers-sooner-when-an-agent-gets-stuck-preparing-a-.md +6 -0
  53. package/corpus/templates/plan/app/global.css +1 -1
  54. package/dist/a2a/caller-auth.d.ts +1 -0
  55. package/dist/a2a/caller-auth.d.ts.map +1 -1
  56. package/dist/a2a/caller-auth.js +34 -8
  57. package/dist/a2a/caller-auth.js.map +1 -1
  58. package/dist/a2a/client.d.ts +4 -0
  59. package/dist/a2a/client.d.ts.map +1 -1
  60. package/dist/a2a/client.js +148 -58
  61. package/dist/a2a/client.js.map +1 -1
  62. package/dist/agent/production-agent.d.ts.map +1 -1
  63. package/dist/agent/production-agent.js +19 -4
  64. package/dist/agent/production-agent.js.map +1 -1
  65. package/dist/agent/types.d.ts +1 -0
  66. package/dist/agent/types.d.ts.map +1 -1
  67. package/dist/agent/types.js.map +1 -1
  68. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  69. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  70. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  71. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  72. package/dist/cli/recap.d.ts +3 -3
  73. package/dist/cli/recap.d.ts.map +1 -1
  74. package/dist/cli/recap.js +41 -34
  75. package/dist/cli/recap.js.map +1 -1
  76. package/dist/client/PoweredByBadge.d.ts +9 -3
  77. package/dist/client/PoweredByBadge.d.ts.map +1 -1
  78. package/dist/client/PoweredByBadge.js +47 -15
  79. package/dist/client/PoweredByBadge.js.map +1 -1
  80. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  81. package/dist/client/agent-chat-adapter.js +5 -1
  82. package/dist/client/agent-chat-adapter.js.map +1 -1
  83. package/dist/client/i18n.d.ts +1 -1
  84. package/dist/client/i18n.d.ts.map +1 -1
  85. package/dist/client/i18n.js +6 -2
  86. package/dist/client/i18n.js.map +1 -1
  87. package/dist/client/sse-event-processor.d.ts +2 -0
  88. package/dist/client/sse-event-processor.d.ts.map +1 -1
  89. package/dist/client/sse-event-processor.js +121 -5
  90. package/dist/client/sse-event-processor.js.map +1 -1
  91. package/dist/collab/routes.d.ts +1 -1
  92. package/dist/file-upload/builder.d.ts.map +1 -1
  93. package/dist/file-upload/builder.js +22 -8
  94. package/dist/file-upload/builder.js.map +1 -1
  95. package/dist/file-upload/types.d.ts +5 -1
  96. package/dist/file-upload/types.d.ts.map +1 -1
  97. package/dist/file-upload/types.js.map +1 -1
  98. package/dist/integrations/a2a-continuation-processor.js +43 -18
  99. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  100. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  101. package/dist/mcp/builtin-tools.js +3 -0
  102. package/dist/mcp/builtin-tools.js.map +1 -1
  103. package/dist/mcp/org-directory.d.ts.map +1 -1
  104. package/dist/mcp/org-directory.js +58 -20
  105. package/dist/mcp/org-directory.js.map +1 -1
  106. package/dist/scripts/call-agent.d.ts.map +1 -1
  107. package/dist/scripts/call-agent.js +2 -2
  108. package/dist/scripts/call-agent.js.map +1 -1
  109. package/package.json +1 -1
@@ -49,6 +49,7 @@ export interface SSEEvent {
49
49
  /** Server-assigned call identifier emitted on tool_start / tool_done events. */
50
50
  id?: string;
51
51
  label?: string;
52
+ progressBytes?: number;
52
53
  input?: Record<string, string>;
53
54
  result?: string;
54
55
  isError?: boolean;
@@ -144,9 +145,58 @@ export class AgentAutoContinueSignal extends Error {
144
145
  }
145
146
 
146
147
  export const SSE_NO_PROGRESS_TIMEOUT_MS = 75_000;
148
+ export const SSE_ACTION_PREPARATION_STALL_TIMEOUT_MS = 90_000;
147
149
 
148
150
  type ActivityTrailEntry = AgentActivityTrailEntry;
149
151
 
152
+ type PreparingActionState = {
153
+ tool?: string;
154
+ startedAt?: number;
155
+ /**
156
+ * Timestamp of the last real streaming progress for the in-preparation tool
157
+ * input. The server emits a throttled `activity` heartbeat per
158
+ * `tool-input-delta`, so while the model is actively streaming a (possibly
159
+ * very large) tool argument this keeps advancing. The stall guard measures
160
+ * silence from HERE — not from `startedAt` — so a legitimately large, still-
161
+ * streaming input is never aborted; only genuine silence (keepalive-only, no
162
+ * further deltas) can trip it.
163
+ */
164
+ lastProgressAt?: number;
165
+ };
166
+
167
+ function formatProgressBytes(bytes: number): string {
168
+ if (bytes < 1024) return `${bytes} B`;
169
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
170
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
171
+ }
172
+
173
+ function activityProgressBytes(ev: SSEEvent): number | undefined {
174
+ return typeof ev.progressBytes === "number" &&
175
+ Number.isFinite(ev.progressBytes) &&
176
+ ev.progressBytes >= 0
177
+ ? Math.floor(ev.progressBytes)
178
+ : undefined;
179
+ }
180
+
181
+ function isPreparingActionActivity(ev: SSEEvent): boolean {
182
+ if (ev.type !== "activity") return false;
183
+ const label = (ev.label ?? "").trim().toLowerCase();
184
+ return label.startsWith("preparing ") && label.includes(" action");
185
+ }
186
+
187
+ function baseActivityLabel(ev: SSEEvent, tool?: string): string {
188
+ return humanizeToolLabelText(ev.label ?? "Working", tool);
189
+ }
190
+
191
+ function visibleActivityLabel(ev: SSEEvent, tool?: string): string {
192
+ const label = baseActivityLabel(ev, tool);
193
+ const progressBytes = activityProgressBytes(ev);
194
+ if (progressBytes === undefined || !isPreparingActionActivity(ev)) {
195
+ return label;
196
+ }
197
+ return `${label} (${formatProgressBytes(progressBytes)} streamed)`;
198
+ }
199
+
150
200
  function findPendingToolCallIndex(
151
201
  content: ContentPart[],
152
202
  toolName: string,
@@ -199,6 +249,53 @@ function appendActivityTrail(
199
249
  }
200
250
  }
201
251
 
252
+ function updatePreparingActionState(
253
+ state: PreparingActionState,
254
+ ev: SSEEvent,
255
+ now: number,
256
+ ) {
257
+ if (ev.type === "activity" && isPreparingActionActivity(ev)) {
258
+ const tool = ev.tool?.trim() || undefined;
259
+ if (!tool) return;
260
+ if (state.tool !== tool || state.startedAt === undefined) {
261
+ state.tool = tool;
262
+ state.startedAt = now;
263
+ }
264
+ // Every tool-input activity is a throttled proof the model is still
265
+ // streaming this action's argument. Treat it as progress so large inputs
266
+ // that take many seconds/minutes to stream are NOT flagged as stalled.
267
+ state.lastProgressAt = now;
268
+ return;
269
+ }
270
+
271
+ if (
272
+ ev.type === "clear" ||
273
+ ev.type === "text" ||
274
+ ev.type === "tool_start" ||
275
+ ev.type === "tool_done" ||
276
+ ev.type === "done" ||
277
+ ev.type === "error" ||
278
+ ev.type === "missing_api_key"
279
+ ) {
280
+ state.tool = undefined;
281
+ state.startedAt = undefined;
282
+ state.lastProgressAt = undefined;
283
+ }
284
+ }
285
+
286
+ function hasStalledPreparingAction(state: PreparingActionState, now: number) {
287
+ // Fire only when a tool input has gone SILENT — no further streaming deltas
288
+ // for the whole window — never merely because a large input has been
289
+ // streaming for a long time. `lastProgressAt` advances on every delta
290
+ // heartbeat, so an actively-streaming large output keeps resetting this and
291
+ // survives; a genuinely stuck prep (keepalive-only, no deltas) trips it.
292
+ return (
293
+ state.tool !== undefined &&
294
+ state.lastProgressAt !== undefined &&
295
+ now - state.lastProgressAt >= SSE_ACTION_PREPARATION_STALL_TIMEOUT_MS
296
+ );
297
+ }
298
+
202
299
  async function readChunkWithProgressTimeout(
203
300
  reader: ReadableStreamDefaultReader<Uint8Array>,
204
301
  lastMeaningfulEventAt: number,
@@ -491,7 +588,7 @@ export function processEvent(
491
588
 
492
589
  if (ev.type === "activity") {
493
590
  const tool = ev.tool?.trim() || undefined;
494
- const label = humanizeToolLabelText(ev.label ?? "Working", tool);
591
+ const label = visibleActivityLabel(ev, tool);
495
592
  if (typeof window !== "undefined") {
496
593
  window.dispatchEvent(
497
594
  new CustomEvent("agent-chat:activity", {
@@ -941,6 +1038,7 @@ export async function* readSSEStream(
941
1038
  let buf = "";
942
1039
  let lastMeaningfulEventAt = Date.now();
943
1040
  const activityTrail: ActivityTrailEntry[] = [];
1041
+ const preparingActionState: PreparingActionState = {};
944
1042
  const processEventState: ProcessEventState = {
945
1043
  completedToolsAfterLastAssistantText: new Set(),
946
1044
  };
@@ -1000,7 +1098,9 @@ export async function* readSSEStream(
1000
1098
  continue;
1001
1099
  }
1002
1100
  sawDataEvent = true;
1003
- lastMeaningfulEventAt = Date.now();
1101
+ const now = Date.now();
1102
+ lastMeaningfulEventAt = now;
1103
+ updatePreparingActionState(preparingActionState, ev, now);
1004
1104
 
1005
1105
  // Track sequence number for reconnection
1006
1106
  if (ev.seq !== undefined && onSeq) {
@@ -1012,7 +1112,7 @@ export async function* readSSEStream(
1012
1112
  } else if (ev.type === "activity") {
1013
1113
  const tool = ev.tool?.trim() || undefined;
1014
1114
  appendActivityTrail(activityTrail, {
1015
- label: humanizeToolLabelText(ev.label ?? "Working", tool),
1115
+ label: baseActivityLabel(ev, tool),
1016
1116
  ...(tool ? { tool } : {}),
1017
1117
  });
1018
1118
  } else if (ev.type === "tool_start") {
@@ -1039,6 +1139,12 @@ export async function* readSSEStream(
1039
1139
  );
1040
1140
 
1041
1141
  if (result) yield withStreamMetadata(result);
1142
+ if (hasStalledPreparingAction(preparingActionState, Date.now())) {
1143
+ throw new AgentAutoContinueSignal({
1144
+ reason: "no_progress",
1145
+ activityTrail: [...activityTrail],
1146
+ });
1147
+ }
1042
1148
  if (action === "auto_continue") {
1043
1149
  throw new AgentAutoContinueSignal(
1044
1150
  autoContinue
@@ -1099,6 +1205,8 @@ export async function readSSEStreamRaw(
1099
1205
  const decoder = new TextDecoder();
1100
1206
  let buf = "";
1101
1207
  let lastMeaningfulEventAt = Date.now();
1208
+ const activityTrail: ActivityTrailEntry[] = [];
1209
+ const preparingActionState: PreparingActionState = {};
1102
1210
  const processEventState: ProcessEventState = {
1103
1211
  completedToolsAfterLastAssistantText: new Set(),
1104
1212
  };
@@ -1134,12 +1242,37 @@ export async function readSSEStreamRaw(
1134
1242
  continue;
1135
1243
  }
1136
1244
  sawDataEvent = true;
1137
- lastMeaningfulEventAt = Date.now();
1245
+ const now = Date.now();
1246
+ lastMeaningfulEventAt = now;
1247
+ updatePreparingActionState(preparingActionState, ev, now);
1138
1248
 
1139
1249
  if (ev.seq !== undefined && onSeq) {
1140
1250
  onSeq(ev.seq);
1141
1251
  }
1142
1252
 
1253
+ if (ev.type === "clear") {
1254
+ activityTrail.length = 0;
1255
+ } else if (ev.type === "activity") {
1256
+ const tool = ev.tool?.trim() || undefined;
1257
+ appendActivityTrail(activityTrail, {
1258
+ label: baseActivityLabel(ev, tool),
1259
+ ...(tool ? { tool } : {}),
1260
+ });
1261
+ } else if (ev.type === "tool_start") {
1262
+ const tool = ev.tool ?? "unknown";
1263
+ appendActivityTrail(activityTrail, {
1264
+ label: runningToolLabel(tool),
1265
+ tool,
1266
+ });
1267
+ } else if (ev.type === "tool_done") {
1268
+ const tool = ev.tool ?? "unknown";
1269
+ for (let i = activityTrail.length - 1; i >= 0; i--) {
1270
+ if (activityTrail[i]?.tool === tool) {
1271
+ activityTrail.splice(i, 1);
1272
+ }
1273
+ }
1274
+ }
1275
+
1143
1276
  const { action, autoContinue } = processEvent(
1144
1277
  ev,
1145
1278
  content,
@@ -1159,9 +1292,18 @@ export async function readSSEStreamRaw(
1159
1292
  if (action === "auto_continue") {
1160
1293
  onUpdate([...content]);
1161
1294
  throw new AgentAutoContinueSignal(
1162
- autoContinue ?? { reason: "stream_ended" },
1295
+ autoContinue
1296
+ ? { ...autoContinue, activityTrail: [...activityTrail] }
1297
+ : { reason: "stream_ended", activityTrail: [...activityTrail] },
1163
1298
  );
1164
1299
  }
1300
+ if (hasStalledPreparingAction(preparingActionState, Date.now())) {
1301
+ onUpdate([...content]);
1302
+ throw new AgentAutoContinueSignal({
1303
+ reason: "no_progress",
1304
+ activityTrail: [...activityTrail],
1305
+ });
1306
+ }
1165
1307
  if (
1166
1308
  action === "done" ||
1167
1309
  action === "error" ||
@@ -27,6 +27,15 @@ function makeBody(bytes: Uint8Array, mimeType: string): BodyInit {
27
27
  : (bytes as unknown as BodyInit);
28
28
  }
29
29
 
30
+ function shouldUseSignedUrlUpload(
31
+ bytes: Uint8Array,
32
+ mimeType: string,
33
+ ): boolean {
34
+ return (
35
+ bytes.byteLength > LARGE_FILE_THRESHOLD_BYTES || /^video\//i.test(mimeType)
36
+ );
37
+ }
38
+
30
39
  function fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {
31
40
  const controller = new AbortController();
32
41
  const timer = setTimeout(() => controller.abort(), UPLOAD_TIMEOUT_MS);
@@ -86,6 +95,7 @@ async function uploadLargeFileViaSignedUrl(
86
95
  privateKey,
87
96
  assetId,
88
97
  input.filename,
98
+ { skipCompressionWait: input.skipCompressionWait },
89
99
  );
90
100
  console.log(`[builder-upload] done [${assetId}]: ${url}`);
91
101
  return { url, id, provider: "builder" };
@@ -139,19 +149,21 @@ async function completeBuilderUpload(
139
149
  privateKey: string,
140
150
  assetId: string,
141
151
  filename: string | undefined,
152
+ options?: { skipCompressionWait?: boolean },
142
153
  ): Promise<{ url: string; id?: string }> {
143
154
  const host = builderUploadHost();
144
- const res = await fetchWithTimeout(
145
- new URL("/api/v1/upload/complete", host).toString(),
146
- {
147
- method: "POST",
148
- headers: {
149
- Authorization: `Bearer ${privateKey}`,
150
- "Content-Type": "application/json",
151
- },
152
- body: JSON.stringify({ assetId, name: filename }),
155
+ const url = new URL("/api/v1/upload/complete", host);
156
+ if (options?.skipCompressionWait) {
157
+ url.searchParams.set("skipCompressionWait", "true");
158
+ }
159
+ const res = await fetchWithTimeout(url.toString(), {
160
+ method: "POST",
161
+ headers: {
162
+ Authorization: `Bearer ${privateKey}`,
163
+ "Content-Type": "application/json",
153
164
  },
154
- );
165
+ body: JSON.stringify({ assetId, name: filename }),
166
+ });
155
167
  await assertOk(res, "Builder.io upload complete failed");
156
168
  const json = (await res.json()) as { url?: string; id?: string };
157
169
  if (!json.url) throw new Error("Builder.io upload/complete returned no URL");
@@ -204,7 +216,8 @@ export const builderFileUploadProvider: FileUploadProvider = {
204
216
  id: "builder",
205
217
  name: "Builder.io",
206
218
  isConfigured: () => !!process.env.BUILDER_PRIVATE_KEY,
207
- upload: async ({ data, filename, mimeType }: FileUploadInput) => {
219
+ upload: async (input: FileUploadInput) => {
220
+ const { data, filename, mimeType } = input;
208
221
  const { resolveBuilderPrivateKey } =
209
222
  await import("../server/credential-provider.js");
210
223
  const privateKey = await resolveBuilderPrivateKey();
@@ -226,9 +239,9 @@ export const builderFileUploadProvider: FileUploadProvider = {
226
239
  data instanceof Uint8Array ? data : new Uint8Array(data as any);
227
240
  const mb = (bytes.byteLength / (1024 * 1024)).toFixed(1);
228
241
 
229
- if (bytes.byteLength > LARGE_FILE_THRESHOLD_BYTES) {
242
+ if (shouldUseSignedUrlUpload(bytes, bareMimeType)) {
230
243
  return uploadLargeFileViaSignedUrl(
231
- { data, filename, mimeType },
244
+ input,
232
245
  privateKey,
233
246
  bareMimeType,
234
247
  bytes,
@@ -241,6 +254,9 @@ export const builderFileUploadProvider: FileUploadProvider = {
241
254
 
242
255
  const url = new URL("/api/v1/upload", builderUploadHost());
243
256
  if (filename) url.searchParams.set("name", filename);
257
+ if (input.skipCompressionWait) {
258
+ url.searchParams.set("skipCompressionWait", "true");
259
+ }
244
260
 
245
261
  const response = await uploadSmallFile(url, {
246
262
  method: "POST",
@@ -366,7 +382,7 @@ export const builderFileUploadProvider: FileUploadProvider = {
366
382
  : new Error("GCS PUT failed after retries");
367
383
  },
368
384
 
369
- async completeSession(session, filename) {
385
+ async completeSession(session, filename, options) {
370
386
  const { resolveBuilderPrivateKey } =
371
387
  await import("../server/credential-provider.js");
372
388
  const privateKey = await resolveBuilderPrivateKey();
@@ -378,6 +394,11 @@ export const builderFileUploadProvider: FileUploadProvider = {
378
394
  privateKey,
379
395
  assetId,
380
396
  filename,
397
+ {
398
+ skipCompressionWait:
399
+ options?.skipCompressionWait ||
400
+ session.meta.skipCompressionWait === true,
401
+ },
381
402
  );
382
403
  console.log(`[builder-resumable] upload complete: ${url}`);
383
404
  return url;
@@ -16,6 +16,8 @@ export interface FileUploadInput {
16
16
  mimeType?: string;
17
17
  /** Optional owner email for per-user scoping in fallback storage. */
18
18
  ownerEmail?: string;
19
+ /** Builder.io upload hint: return after asset registration instead of waiting for server-side compression. */
20
+ skipCompressionWait?: boolean;
19
21
  }
20
22
 
21
23
  export interface FileUploadResult {
@@ -77,6 +79,7 @@ export interface FileUploadProvider {
77
79
  completeSession(
78
80
  session: ResumableUploadSession,
79
81
  filename: string,
82
+ options?: { skipCompressionWait?: boolean },
80
83
  ): Promise<string>;
81
84
  };
82
85
  }
@@ -150,11 +150,11 @@ async function processClaimedContinuation(
150
150
  return;
151
151
  }
152
152
 
153
- const client = new A2AClient(
154
- continuation.agentUrl,
155
- await signContinuationToken(continuation),
156
- { requestTimeoutMs: POLL_REQUEST_TIMEOUT_MS },
157
- );
153
+ const auth = await signContinuationToken(continuation);
154
+ const client = new A2AClient(continuation.agentUrl, auth.apiKey, {
155
+ requestTimeoutMs: POLL_REQUEST_TIMEOUT_MS,
156
+ ...(auth.apiKeyFallbacks ? { fallbackApiKeys: auth.apiKeyFallbacks } : {}),
157
+ });
158
158
  const deadline = Date.now() + PROCESSOR_WAIT_MS;
159
159
  let task: Task | null = null;
160
160
 
@@ -438,28 +438,35 @@ function sanitizeFailureReason(reason: string): string {
438
438
 
439
439
  async function signContinuationToken(
440
440
  continuation: A2AContinuation,
441
- ): Promise<string | undefined> {
441
+ ): Promise<{ apiKey?: string; apiKeyFallbacks?: string[] }> {
442
442
  if (continuation.a2aAuthToken === "") {
443
- return undefined;
443
+ return {};
444
444
  }
445
445
 
446
446
  const storedToken = continuation.a2aAuthToken;
447
- if (storedToken && !isLikelyJwt(storedToken)) return storedToken;
448
-
449
- const freshToken = await signFreshContinuationToken(continuation);
450
- if (freshToken) return freshToken;
451
- if (!storedToken) return undefined;
447
+ if (storedToken && !isLikelyJwt(storedToken)) return { apiKey: storedToken };
448
+
449
+ const freshTokens = await signFreshContinuationTokens(continuation);
450
+ if (freshTokens.length > 0) {
451
+ return {
452
+ apiKey: freshTokens[0],
453
+ ...(freshTokens.length > 1
454
+ ? { apiKeyFallbacks: freshTokens.slice(1) }
455
+ : {}),
456
+ };
457
+ }
458
+ if (!storedToken) return {};
452
459
 
453
460
  // Older continuations may have persisted the initial short-lived JWT. Avoid
454
461
  // replaying it forever after expiry; opaque legacy bearer keys can still be
455
462
  // reused because we cannot re-mint those.
456
- if (isLikelyJwt(storedToken)) return undefined;
457
- return storedToken;
463
+ if (isLikelyJwt(storedToken)) return {};
464
+ return { apiKey: storedToken };
458
465
  }
459
466
 
460
- async function signFreshContinuationToken(
467
+ async function signFreshContinuationTokens(
461
468
  continuation: A2AContinuation,
462
- ): Promise<string | undefined> {
469
+ ): Promise<string[]> {
463
470
  let orgDomain: string | undefined;
464
471
  let orgSecret: string | undefined;
465
472
  if (continuation.orgId) {
@@ -472,17 +479,35 @@ async function signFreshContinuationToken(
472
479
  }
473
480
 
474
481
  if (!continuation.ownerEmail || !(orgSecret || process.env.A2A_SECRET)) {
475
- return undefined;
482
+ return [];
476
483
  }
477
484
 
478
- try {
479
- return await signA2AToken(continuation.ownerEmail, orgDomain, orgSecret, {
480
- expiresIn: "30m",
481
- preferGlobalSecret: !orgSecret,
482
- });
483
- } catch {
484
- return undefined;
485
+ const tokens: string[] = [];
486
+ const add = (token: string | undefined) => {
487
+ if (token && !tokens.includes(token)) tokens.push(token);
488
+ };
489
+
490
+ if (process.env.A2A_SECRET?.trim()) {
491
+ try {
492
+ add(
493
+ await signA2AToken(continuation.ownerEmail, orgDomain, orgSecret, {
494
+ expiresIn: "30m",
495
+ preferGlobalSecret: true,
496
+ }),
497
+ );
498
+ } catch {}
499
+ }
500
+ if (orgSecret) {
501
+ try {
502
+ add(
503
+ await signA2AToken(continuation.ownerEmail, orgDomain, orgSecret, {
504
+ expiresIn: "30m",
505
+ preferGlobalSecret: false,
506
+ }),
507
+ );
508
+ } catch {}
485
509
  }
510
+ return tokens;
486
511
  }
487
512
 
488
513
  function isLikelyJwt(token: string): boolean {
@@ -260,6 +260,9 @@ async function createA2AClientForAskApp(origin: string): Promise<{
260
260
  return {
261
261
  client: new A2AClient(origin, auth.apiKey, {
262
262
  requestTimeoutMs: ASK_APP_A2A_REQUEST_TIMEOUT_MS,
263
+ ...(auth.apiKeyFallbacks
264
+ ? { fallbackApiKeys: auth.apiKeyFallbacks }
265
+ : {}),
263
266
  }),
264
267
  metadata,
265
268
  };
@@ -146,6 +146,15 @@ function scopedCacheKey(
146
146
  ].join("|");
147
147
  }
148
148
 
149
+ function authTokenAttempts(auth: {
150
+ apiKey?: string;
151
+ apiKeyFallbacks?: string[];
152
+ }): string[] {
153
+ return [auth.apiKey, ...(auth.apiKeyFallbacks ?? [])].filter(
154
+ (token): token is string => typeof token === "string" && token.length > 0,
155
+ );
156
+ }
157
+
149
158
  function serviceScopedCacheKey(origin: string, orgId: string): string {
150
159
  return [origin, `service-org:${orgId}`].join("|");
151
160
  }
@@ -200,7 +209,8 @@ export async function fetchOrgApps(opts?: {
200
209
  const auth = serviceOrgId
201
210
  ? await resolveOrgDirectoryServiceAuth(serviceOrgId)
202
211
  : await resolveOrgDirectoryCallerAuth();
203
- if (!auth.apiKey) {
212
+ const attempts = authTokenAttempts(auth);
213
+ if (attempts.length === 0) {
204
214
  // No signed token available (no A2A secret / no caller identity) — the
205
215
  // directory requires the org bearer, so degrade silently to local-only.
206
216
  return [];
@@ -215,19 +225,23 @@ export async function fetchOrgApps(opts?: {
215
225
  }
216
226
  }
217
227
 
218
- const res = await fetch(`${origin}/_agent-native/org/apps`, {
219
- method: "GET",
220
- headers: {
221
- Authorization: `Bearer ${auth.apiKey}`,
222
- Accept: "application/json",
223
- },
224
- signal: AbortSignal.timeout(4000),
225
- });
226
- if (res.ok) {
227
- const json = (await res.json()) as { apps?: unknown };
228
- const list = Array.isArray(json?.apps) ? json.apps : [];
229
- apps = list.map(normalizeApp).filter((a): a is OrgApp => a !== null);
230
- ttl = SUCCESS_TTL_MS;
228
+ for (let i = 0; i < attempts.length; i++) {
229
+ const res = await fetch(`${origin}/_agent-native/org/apps`, {
230
+ method: "GET",
231
+ headers: {
232
+ Authorization: `Bearer ${attempts[i]}`,
233
+ Accept: "application/json",
234
+ },
235
+ signal: AbortSignal.timeout(4000),
236
+ });
237
+ if (res.ok) {
238
+ const json = (await res.json()) as { apps?: unknown };
239
+ const list = Array.isArray(json?.apps) ? json.apps : [];
240
+ apps = list.map(normalizeApp).filter((a): a is OrgApp => a !== null);
241
+ ttl = SUCCESS_TTL_MS;
242
+ break;
243
+ }
244
+ if (res.status !== 401 || i >= attempts.length - 1) break;
231
245
  }
232
246
  // Non-2xx ⇒ leave apps=[] with the short EMPTY_TTL (silent degrade).
233
247
  } catch {
@@ -252,6 +266,7 @@ export function _resetOrgDirectoryCache(): void {
252
266
 
253
267
  async function resolveOrgDirectoryCallerAuth(): Promise<{
254
268
  apiKey?: string;
269
+ apiKeyFallbacks?: string[];
255
270
  userEmail?: string;
256
271
  orgId?: string;
257
272
  orgDomain?: string;
@@ -266,6 +281,7 @@ async function resolveOrgDirectoryCallerAuth(): Promise<{
266
281
 
267
282
  async function resolveOrgDirectoryServiceAuth(orgId: string): Promise<{
268
283
  apiKey?: string;
284
+ apiKeyFallbacks?: string[];
269
285
  userEmail?: string;
270
286
  orgId?: string;
271
287
  orgDomain?: string;
@@ -285,12 +301,42 @@ async function resolveOrgDirectoryServiceAuth(orgId: string): Promise<{
285
301
  import("./connect-store.js"),
286
302
  ]);
287
303
  const userEmail = serviceIdentityEmail("mcp-client", trimmedOrgId);
288
- const apiKey = await signA2AToken(userEmail, orgDomain, orgSecret, {
289
- expiresIn: "5m",
290
- preferGlobalSecret: !orgSecret,
291
- extraClaims: { org_id: trimmedOrgId },
292
- });
293
- return { apiKey, userEmail, orgId: trimmedOrgId, orgDomain };
304
+ const apiKeyAttempts: string[] = [];
305
+ const addApiKeyAttempt = (token: string | undefined) => {
306
+ if (!token || apiKeyAttempts.includes(token)) return;
307
+ apiKeyAttempts.push(token);
308
+ };
309
+ if (process.env.A2A_SECRET?.trim()) {
310
+ try {
311
+ addApiKeyAttempt(
312
+ await signA2AToken(userEmail, orgDomain, orgSecret, {
313
+ expiresIn: "5m",
314
+ preferGlobalSecret: true,
315
+ extraClaims: { org_id: trimmedOrgId },
316
+ }),
317
+ );
318
+ } catch {}
319
+ }
320
+ if (orgSecret) {
321
+ try {
322
+ addApiKeyAttempt(
323
+ await signA2AToken(userEmail, orgDomain, orgSecret, {
324
+ expiresIn: "5m",
325
+ preferGlobalSecret: false,
326
+ extraClaims: { org_id: trimmedOrgId },
327
+ }),
328
+ );
329
+ } catch {}
330
+ }
331
+ return {
332
+ apiKey: apiKeyAttempts[0],
333
+ ...(apiKeyAttempts.length > 1
334
+ ? { apiKeyFallbacks: apiKeyAttempts.slice(1) }
335
+ : {}),
336
+ userEmail,
337
+ orgId: trimmedOrgId,
338
+ orgDomain,
339
+ };
294
340
  } catch {
295
341
  return { orgId: trimmedOrgId, orgDomain };
296
342
  }
@@ -1,6 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
- import { A2ATaskTimeoutError, callAgent, signA2AToken } from "../a2a/client.js";
3
+ import {
4
+ A2ATaskTimeoutError,
5
+ callAgent,
6
+ shouldPreferGlobalA2ASecret,
7
+ signA2AToken,
8
+ } from "../a2a/client.js";
4
9
  import {
5
10
  formatLlmCredentialErrorMessage,
6
11
  isLlmCredentialError,
@@ -162,7 +167,7 @@ export async function run(
162
167
  callerOrgSecret,
163
168
  {
164
169
  expiresIn: INTEGRATION_A2A_TOKEN_TTL,
165
- preferGlobalSecret: !callerOrgSecret,
170
+ preferGlobalSecret: shouldPreferGlobalA2ASecret(callerOrgSecret),
166
171
  },
167
172
  );
168
173
  } catch {}