@superblocksteam/gateway 2.0.165-next.0 → 2.0.166-next.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.
Files changed (59) hide show
  1. package/dist/agents/resolve-orchestrator-url.d.ts +19 -1
  2. package/dist/agents/resolve-orchestrator-url.js +71 -20
  3. package/dist/agents/resolve-orchestrator-url.js.map +1 -1
  4. package/dist/capabilities/integration-metadata.d.ts +3 -0
  5. package/dist/capabilities/integration-metadata.js +34 -11
  6. package/dist/capabilities/integration-metadata.js.map +1 -1
  7. package/dist/capabilities/lifecycle.d.ts +16 -1
  8. package/dist/capabilities/lifecycle.js +72 -25
  9. package/dist/capabilities/lifecycle.js.map +1 -1
  10. package/dist/capabilities/query-integration.d.ts +5 -0
  11. package/dist/capabilities/query-integration.js +61 -18
  12. package/dist/capabilities/query-integration.js.map +1 -1
  13. package/dist/{transports/mcp → capture}/cowork-editor-url.d.ts +2 -2
  14. package/dist/capture/cowork-editor-url.js +12 -0
  15. package/dist/capture/cowork-editor-url.js.map +1 -0
  16. package/dist/integrations/profile-key.d.ts +50 -6
  17. package/dist/integrations/profile-key.js +101 -22
  18. package/dist/integrations/profile-key.js.map +1 -1
  19. package/dist/integrations/resolve-profile.d.ts +30 -0
  20. package/dist/integrations/resolve-profile.js +104 -0
  21. package/dist/integrations/resolve-profile.js.map +1 -0
  22. package/dist/orchestrator/client.d.ts +1 -1
  23. package/dist/orchestrator/client.js +1 -1
  24. package/dist/orchestrator/client.js.map +1 -1
  25. package/dist/sabs/editor-client-methods.d.ts +7 -2
  26. package/dist/sabs/editor-client-methods.js +10 -2
  27. package/dist/sabs/editor-client-methods.js.map +1 -1
  28. package/dist/sabs/recent-tool-runs.d.ts +1 -1
  29. package/dist/sabs/recent-tool-runs.js +2 -2
  30. package/dist/sabs/recent-tool-runs.js.map +1 -1
  31. package/dist/sabs/websocket-session-peer.d.ts +31 -0
  32. package/dist/sabs/websocket-session-peer.js +202 -35
  33. package/dist/sabs/websocket-session-peer.js.map +1 -1
  34. package/dist/server/client.d.ts +67 -15
  35. package/dist/server/client.js +403 -51
  36. package/dist/server/client.js.map +1 -1
  37. package/dist/telemetry/metrics.d.ts +14 -0
  38. package/dist/telemetry/metrics.js +25 -0
  39. package/dist/telemetry/metrics.js.map +1 -1
  40. package/dist/transports/mcp/instructions/tools/check-app-progress.js +6 -2
  41. package/dist/transports/mcp/instructions/tools/check-app-progress.js.map +1 -1
  42. package/dist/transports/mcp/instructions/tools/copy.d.ts +2 -0
  43. package/dist/transports/mcp/instructions/tools/copy.js +31 -1
  44. package/dist/transports/mcp/instructions/tools/copy.js.map +1 -1
  45. package/dist/transports/mcp/instructions/tools/edit-app.js +4 -1
  46. package/dist/transports/mcp/instructions/tools/edit-app.js.map +1 -1
  47. package/dist/transports/mcp/instructions/tools/get-integration-metadata.js +3 -0
  48. package/dist/transports/mcp/instructions/tools/get-integration-metadata.js.map +1 -1
  49. package/dist/transports/mcp/instructions/tools/start-app.js +4 -1
  50. package/dist/transports/mcp/instructions/tools/start-app.js.map +1 -1
  51. package/dist/transports/mcp/mount.d.ts +1 -1
  52. package/dist/transports/mcp/mount.js +80 -10
  53. package/dist/transports/mcp/mount.js.map +1 -1
  54. package/dist/transports/mcp/plan-approval.d.ts +29 -0
  55. package/dist/transports/mcp/plan-approval.js +72 -18
  56. package/dist/transports/mcp/plan-approval.js.map +1 -1
  57. package/package.json +5 -5
  58. package/dist/transports/mcp/cowork-editor-url.js +0 -10
  59. package/dist/transports/mcp/cowork-editor-url.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { uploadAttachmentToServerApi } from "@superblocksteam/shared";
2
- import { recordIdentityReadOutcome } from "../telemetry/metrics.js";
2
+ import { recordFactsFlagBootstrapOutcome, recordIdentityReadOutcome, recordServerReadRetryOutcome, } from "../telemetry/metrics.js";
3
3
  import { withGuardedIdentityReads, } from "./identity-reads.js";
4
4
  const MAX_DETAIL_LENGTH = 300;
5
5
  /**
@@ -31,12 +31,98 @@ function detailSuffix(body) {
31
31
  }
32
32
  export class ServerHttpError extends Error {
33
33
  status;
34
- constructor(message, status) {
34
+ retryAfterMs;
35
+ constructor(message, status, retryAfterMs) {
35
36
  super(message);
36
37
  this.status = status;
38
+ this.retryAfterMs = retryAfterMs;
37
39
  this.name = "ServerHttpError";
38
40
  }
39
41
  }
42
+ /**
43
+ * This client gave up on a request before the server answered it.
44
+ *
45
+ * Told apart from a lost connection because the two deserve different
46
+ * treatment on a retry: a connection that was refused or dropped is cheap to
47
+ * try again, while repeating a request that already burned its whole bound
48
+ * multiplies the wait a caller is sitting through.
49
+ */
50
+ export class ServerTimeoutError extends Error {
51
+ constructor(message, options) {
52
+ super(message, options);
53
+ this.name = "ServerTimeoutError";
54
+ }
55
+ }
56
+ const RETRYABLE_TRANSPORT_CODES = new Set([
57
+ "EAI_AGAIN",
58
+ "ECONNABORTED",
59
+ "ECONNREFUSED",
60
+ "ECONNRESET",
61
+ "EHOSTUNREACH",
62
+ "ENETDOWN",
63
+ "ENETUNREACH",
64
+ "EPIPE",
65
+ "ETIMEDOUT",
66
+ "UND_ERR_BODY_TIMEOUT",
67
+ "UND_ERR_CONNECT_TIMEOUT",
68
+ "UND_ERR_HEADERS_TIMEOUT",
69
+ "UND_ERR_SOCKET",
70
+ ]);
71
+ const UNSENT_TRANSPORT_CODES = new Set([
72
+ "EAI_AGAIN",
73
+ "ECONNREFUSED",
74
+ "EHOSTUNREACH",
75
+ "ENETDOWN",
76
+ "ENETUNREACH",
77
+ "UND_ERR_CONNECT_TIMEOUT",
78
+ ]);
79
+ function errorCode(error, depth = 0) {
80
+ if (depth >= 4 || !error || typeof error !== "object") {
81
+ return undefined;
82
+ }
83
+ const { cause, code } = error;
84
+ if (typeof code === "string") {
85
+ return code;
86
+ }
87
+ const causeCode = errorCode(cause, depth + 1);
88
+ if (causeCode) {
89
+ return causeCode;
90
+ }
91
+ if (error instanceof AggregateError) {
92
+ for (const nested of error.errors) {
93
+ const nestedCode = errorCode(nested, depth + 1);
94
+ if (nestedCode) {
95
+ return nestedCode;
96
+ }
97
+ }
98
+ }
99
+ return undefined;
100
+ }
101
+ /**
102
+ * The connection failed: refused, reset, or dropped before the answer was
103
+ * complete. Named so a retry can cover it without also covering a response
104
+ * that did arrive and turned out to be unreadable.
105
+ *
106
+ * Most failures do not prove whether the request reached a server, so repeat
107
+ * safety remains the call site's decision. `requestNotSent` is reserved for
108
+ * pre-connect failures whose error code proves otherwise.
109
+ */
110
+ export class ServerTransportError extends Error {
111
+ code;
112
+ requestNotSent;
113
+ retryable;
114
+ constructor(message, options) {
115
+ const code = errorCode(options.cause);
116
+ super(code ? `${message} (${code})` : message, options);
117
+ this.name = "ServerTransportError";
118
+ this.code = code;
119
+ this.requestNotSent =
120
+ options.requestNotSent === true &&
121
+ code !== undefined &&
122
+ UNSENT_TRANSPORT_CODES.has(code);
123
+ this.retryable = code !== undefined && RETRYABLE_TRANSPORT_CODES.has(code);
124
+ }
125
+ }
40
126
  /**
41
127
  * Generous enough for a sandbox start.
42
128
  *
@@ -73,21 +159,142 @@ const DEFAULT_IDENTITY_TIMEOUT_MS = 30_000;
73
159
  * kept going straight at an endpoint already asking for less load.
74
160
  */
75
161
  function identityEndpointUnhealthy(error) {
76
- return !(error instanceof ServerHttpError &&
77
- error.status < 500 &&
78
- error.status !== 429);
162
+ return !(error instanceof ServerHttpError && !isTransientHttpStatus(error.status));
163
+ }
164
+ function isTransientHttpStatus(status) {
165
+ return status >= 500 || status === 429;
79
166
  }
80
167
  /**
81
- * Extra attempts for reads that failed with a server-side error.
168
+ * How long a read waits before each re-send, by what failed it.
169
+ *
170
+ * Only reads are retried, and only because the call sites that leave `retry`
171
+ * on are pure lookups: a repeat of one changes nothing. A failed POST may well
172
+ * have taken effect, and re-sending one can deploy or commit twice; a read
173
+ * that provisions, or that already sits in its own poll loop, opts out.
174
+ *
175
+ * A transient status is a fast answer from a route that is up, so a short
176
+ * pause is enough. 429 counts as transient alongside 5xx: it is the route
177
+ * reporting its own capacity, and backing off is the whole point of it.
178
+ * Retry-After replaces that pause when it is at most five seconds; a longer
179
+ * server window is surfaced immediately rather than parking a tool call.
180
+ *
181
+ * A lost connection is usually a deployment rolling through the API, which
182
+ * refuses and resets connections for a few seconds. This schedule waits that
183
+ * out, and still finishes well inside the ten seconds a tool call waits on
184
+ * identity. A timeout is never retried: that attempt already spent its whole
185
+ * bound, and sending it again spends another.
82
186
  *
83
- * Only GETs are retried: they carry no side effects, so a repeat is free. A
84
- * failed POST may well have taken effect, and re-sending one can deploy or
85
- * commit twice.
187
+ * Each delay is one retry: transient statuses get two, lost connections get
188
+ * four.
86
189
  */
87
- const READ_RETRY_ATTEMPTS = 2;
88
- const READ_RETRY_BASE_DELAY_MS = 100;
89
- function delay(ms) {
90
- return new Promise((resolve) => setTimeout(resolve, ms));
190
+ const TRANSIENT_STATUS_RETRY_DELAYS_MS = [100, 200];
191
+ const LOST_CONNECTION_RETRY_DELAYS_MS = [250, 500, 1_000, 2_000];
192
+ const MAX_RETRY_AFTER_MS = 5_000;
193
+ const MIN_RETRY_ATTEMPT_MS = 50;
194
+ function jitter(delayMs) {
195
+ return Math.ceil(delayMs * (0.75 + Math.random() * 0.25));
196
+ }
197
+ function jitterAfterMinimum(delayMs) {
198
+ return Math.ceil(delayMs * (1 + Math.random() * 0.25));
199
+ }
200
+ class ReadRetrySchedule {
201
+ lostConnections = 0;
202
+ transientStatuses = 0;
203
+ next(error) {
204
+ if (error instanceof ServerHttpError) {
205
+ if (!isTransientHttpStatus(error.status)) {
206
+ return undefined;
207
+ }
208
+ const delayMs = TRANSIENT_STATUS_RETRY_DELAYS_MS[this.transientStatuses++];
209
+ if (delayMs === undefined) {
210
+ return "exhausted";
211
+ }
212
+ if (error.retryAfterMs === undefined) {
213
+ return { cause: "http", waitMs: jitter(delayMs) };
214
+ }
215
+ return error.retryAfterMs > MAX_RETRY_AFTER_MS
216
+ ? undefined
217
+ : {
218
+ cause: "http",
219
+ waitMs: jitterAfterMinimum(Math.max(delayMs, error.retryAfterMs)),
220
+ };
221
+ }
222
+ if (error instanceof ServerTransportError && error.retryable) {
223
+ const delayMs = LOST_CONNECTION_RETRY_DELAYS_MS[this.lostConnections++];
224
+ return delayMs === undefined
225
+ ? "exhausted"
226
+ : { cause: "transport", waitMs: jitter(delayMs) };
227
+ }
228
+ return undefined;
229
+ }
230
+ }
231
+ function parseRetryAfterMs(value) {
232
+ const retryAfter = value?.trim();
233
+ if (!retryAfter) {
234
+ return undefined;
235
+ }
236
+ const seconds = Number(retryAfter);
237
+ if (Number.isFinite(seconds) && seconds >= 0) {
238
+ return Math.ceil(seconds * 1_000);
239
+ }
240
+ const atMs = Date.parse(retryAfter);
241
+ return Number.isNaN(atMs) ? undefined : Math.max(0, atMs - Date.now());
242
+ }
243
+ function withAttemptCount(error, attempts) {
244
+ if (attempts < 2 || !(error instanceof Error)) {
245
+ return error;
246
+ }
247
+ error.message = `${error.message}; ${attempts} attempts failed`;
248
+ return error;
249
+ }
250
+ function delay(ms, signal) {
251
+ if (signal?.aborted) {
252
+ return Promise.reject(signal.reason);
253
+ }
254
+ return new Promise((resolve, reject) => {
255
+ const onAbort = () => {
256
+ clearTimeout(timeout);
257
+ reject(signal?.reason);
258
+ };
259
+ const timeout = setTimeout(() => {
260
+ signal?.removeEventListener("abort", onAbort);
261
+ resolve();
262
+ }, ms);
263
+ signal?.addEventListener("abort", onAbort, { once: true });
264
+ });
265
+ }
266
+ /**
267
+ * One attempt at one request: the signal that bounds it, and the judgement of
268
+ * why it failed, applied alike to the connection and to the body read so a
269
+ * connection lost mid-response counts the same as one refused up front.
270
+ */
271
+ class ServerAttempt {
272
+ path;
273
+ boundMs;
274
+ callerSignal;
275
+ timeoutSignal;
276
+ signal;
277
+ constructor(path, boundMs, callerSignal) {
278
+ this.path = path;
279
+ this.boundMs = boundMs;
280
+ this.callerSignal = callerSignal;
281
+ this.timeoutSignal = AbortSignal.timeout(boundMs);
282
+ this.signal = callerSignal
283
+ ? AbortSignal.any([callerSignal, this.timeoutSignal])
284
+ : this.timeoutSignal;
285
+ }
286
+ /** What to throw for a fetch or body read that rejected with `error`. */
287
+ failure(error, lost, requestNotSent = false) {
288
+ // The caller's own abort is neither our timeout nor a lost connection,
289
+ // and wrapping it as one would hand it to the read retry.
290
+ if (this.callerSignal?.aborted) {
291
+ return error;
292
+ }
293
+ if (this.timeoutSignal.aborted) {
294
+ return new ServerTimeoutError(`Server ${this.path} timed out after ${this.boundMs}ms`, { cause: error });
295
+ }
296
+ return new ServerTransportError(`Server ${this.path} ${lost}: ${error instanceof Error ? error.message : String(error)}`, { cause: error, requestNotSent });
297
+ }
91
298
  }
92
299
  const DEPLOY_STATUSES = [
93
300
  "awaiting_ack",
@@ -188,67 +395,152 @@ export class HttpSuperblocksServerClient {
188
395
  return body.data;
189
396
  }
190
397
  /**
191
- * Issues the request, retrying a transient status when the verb makes that
192
- * safe.
193
- *
194
- * A transient failure on the identity lookup used to fail the whole tool call
195
- * and leave the caller guessing whether their account was broken.
398
+ * Issues the request, sending a read again when what failed it says nothing
399
+ * about the request itself: a transient status, or a connection that was
400
+ * lost. {@link ReadRetrySchedule} decides which failures qualify and for how
401
+ * long.
196
402
  *
197
- * 429 counts as transient alongside 5xx: it is the route reporting its own
198
- * capacity, and backing off is the whole point of it.
403
+ * A transient failure on the identity lookup used to fail the whole tool
404
+ * call and leave the caller guessing whether their account was broken. A
405
+ * lost connection used to fail on first sight, which is how a build's poll
406
+ * loop lost its caller's identity while a deployment rolled through the API.
199
407
  */
200
408
  async request(path, init) {
201
- const retryable = init.retry !== false && (init.method ?? "GET").toUpperCase() === "GET";
202
- let lastError;
203
- for (let attempt = 0; attempt <= (retryable ? READ_RETRY_ATTEMPTS : 0);) {
204
- try {
205
- return await this.send(path, init);
409
+ const method = (init.method ?? "GET").toUpperCase();
410
+ const repeatSafe = init.retry ?? method === "GET";
411
+ const retries = new ReadRetrySchedule();
412
+ const timeoutMs = init.timeoutMs ?? this.timeoutMs;
413
+ const deadlineAtMs = Date.now() + timeoutMs;
414
+ let attempts = 0;
415
+ let lastFailure;
416
+ let retryCause;
417
+ let retryExhausted = false;
418
+ const operation = init.operation ?? "other";
419
+ const timeoutError = (cause = lastFailure) => new ServerTimeoutError(`Server ${path} timed out after ${timeoutMs}ms${attempts > 0 ? `; ${attempts} ${attempts === 1 ? "attempt" : "attempts"} failed` : ""}`, cause === undefined ? undefined : { cause });
420
+ try {
421
+ for (;;) {
422
+ const remainingMs = deadlineAtMs - Date.now();
423
+ if (remainingMs <= 0) {
424
+ retryExhausted = retryCause !== undefined;
425
+ throw timeoutError();
426
+ }
427
+ attempts += 1;
428
+ try {
429
+ const result = await this.send(path, {
430
+ ...init,
431
+ timeoutMs: remainingMs,
432
+ });
433
+ if (retryCause) {
434
+ recordServerReadRetryOutcome({
435
+ cause: retryCause,
436
+ operation,
437
+ outcome: "recovered",
438
+ });
439
+ }
440
+ return result;
441
+ }
442
+ catch (error) {
443
+ if (init.signal?.aborted) {
444
+ throw error;
445
+ }
446
+ if (error instanceof ServerTimeoutError) {
447
+ retryExhausted = retryCause !== undefined;
448
+ throw timeoutError(lastFailure ?? error.cause);
449
+ }
450
+ lastFailure = error;
451
+ const retry = repeatSafe ||
452
+ (method === "GET" &&
453
+ error instanceof ServerTransportError &&
454
+ error.requestNotSent)
455
+ ? retries.next(error)
456
+ : undefined;
457
+ if (retry === undefined || retry === "exhausted") {
458
+ retryExhausted = retryCause !== undefined && retry === "exhausted";
459
+ throw withAttemptCount(error, attempts);
460
+ }
461
+ const remainingAfterAttemptMs = deadlineAtMs - Date.now();
462
+ if (remainingAfterAttemptMs <= 0) {
463
+ retryExhausted = retryCause !== undefined;
464
+ throw timeoutError();
465
+ }
466
+ if (remainingAfterAttemptMs - retry.waitMs < MIN_RETRY_ATTEMPT_MS) {
467
+ retryExhausted = retryCause !== undefined;
468
+ throw withAttemptCount(error, attempts);
469
+ }
470
+ retryCause ??= retry.cause;
471
+ await delay(retry.waitMs, init.signal);
472
+ if (init.signal?.aborted) {
473
+ throw init.signal.reason;
474
+ }
475
+ if (Date.now() >= deadlineAtMs) {
476
+ retryExhausted = true;
477
+ throw timeoutError();
478
+ }
479
+ }
206
480
  }
207
- catch (error) {
208
- lastError = error;
209
- const transient = error instanceof ServerHttpError &&
210
- (error.status >= 500 || error.status === 429);
211
- attempt += 1;
212
- if (!retryable || !transient || attempt > READ_RETRY_ATTEMPTS) {
213
- throw error;
481
+ }
482
+ catch (error) {
483
+ if (retryCause && !init.signal?.aborted) {
484
+ if (retryExhausted) {
485
+ console.error("gateway server read retries exhausted", {
486
+ attempts,
487
+ cause: retryCause,
488
+ message: error instanceof Error ? error.message : String(error),
489
+ operation,
490
+ path,
491
+ });
214
492
  }
215
- await delay(READ_RETRY_BASE_DELAY_MS * attempt);
493
+ recordServerReadRetryOutcome({
494
+ cause: retryCause,
495
+ operation,
496
+ outcome: retryExhausted ? "exhausted" : "terminal",
497
+ });
216
498
  }
499
+ throw error;
217
500
  }
218
- throw lastError;
219
501
  }
220
502
  async send(path, init) {
221
- const { apiKey, headers: callerHeaders, retry: _retry, signal: callerSignal, timeoutMs, ...rest } = init;
222
- const bound = timeoutMs ?? this.timeoutMs;
503
+ const { apiKey, headers: callerHeaders, operation: _operation, retry: _retry, signal: callerSignal, timeoutMs, ...rest } = init;
223
504
  const mergedHeaders = new Headers(callerHeaders);
224
505
  // Auth and content-type are owned by this client — never override from callers.
225
506
  mergedHeaders.set("Authorization", `Bearer ${apiKey}`);
226
507
  if (!mergedHeaders.has("Content-Type")) {
227
508
  mergedHeaders.set("Content-Type", "application/json");
228
509
  }
229
- const timeoutSignal = AbortSignal.timeout(bound);
510
+ const attempt = new ServerAttempt(path, timeoutMs ?? this.timeoutMs, callerSignal);
230
511
  let response;
231
512
  try {
232
513
  response = await fetch(`${this.baseUrl()}${path}`, {
233
514
  ...rest,
234
515
  headers: mergedHeaders,
235
- signal: callerSignal
236
- ? AbortSignal.any([callerSignal, timeoutSignal])
237
- : timeoutSignal,
516
+ signal: attempt.signal,
238
517
  });
239
518
  }
240
519
  catch (error) {
241
- if (timeoutSignal.aborted &&
242
- error instanceof Error &&
243
- error.name === "TimeoutError") {
244
- throw new Error(`Server ${path} timed out after ${bound}ms`);
520
+ throw attempt.failure(error, "could not be reached", true);
521
+ }
522
+ let body;
523
+ try {
524
+ body = await response.text();
525
+ }
526
+ catch (error) {
527
+ const failure = attempt.failure(error, "dropped the connection mid-response");
528
+ const retryAfterMs = parseRetryAfterMs(response.headers.get("Retry-After"));
529
+ if (!response.ok &&
530
+ !callerSignal?.aborted &&
531
+ (response.status === 429 ||
532
+ !isTransientHttpStatus(response.status) ||
533
+ (failure instanceof ServerTransportError && !failure.retryable))) {
534
+ throw new ServerHttpError(`Server ${path} failed: ${response.status}`, response.status, retryAfterMs);
245
535
  }
246
- throw error;
536
+ throw failure;
247
537
  }
248
538
  if (!response.ok) {
249
- throw new ServerHttpError(`Server ${path} failed: ${response.status}${detailSuffix(await response.text().catch(() => ""))}`, response.status);
539
+ throw new ServerHttpError(`Server ${path} failed: ${response.status}${detailSuffix(body)}`, response.status, parseRetryAfterMs(response.headers.get("Retry-After")));
250
540
  }
251
- return (await response.json());
541
+ // A body that arrived whole but cannot be parsed is an answer, not a lost
542
+ // connection: the route is up and will say the same thing again.
543
+ return JSON.parse(body);
252
544
  }
253
545
  /**
254
546
  * The Superblocks user behind a downstream credential.
@@ -266,6 +558,7 @@ export class HttpSuperblocksServerClient {
266
558
  const data = await this.requestData("/api/v1/users/me", {
267
559
  apiKey,
268
560
  method: "GET",
561
+ operation: "identity",
269
562
  timeoutMs: this.identityTimeoutMs,
270
563
  });
271
564
  const user = data.user;
@@ -291,6 +584,7 @@ export class HttpSuperblocksServerClient {
291
584
  const data = await this.requestData("/api/v1/users/me", {
292
585
  apiKey: input.apiKey,
293
586
  method: "GET",
587
+ operation: "identity",
294
588
  // Same route, so the same bound. This one runs on the build path rather
295
589
  // than per tool call, and left at the sandbox timeout a wedged
296
590
  // /users/me held a start_app open for two minutes here instead.
@@ -317,9 +611,17 @@ export class HttpSuperblocksServerClient {
317
611
  url: raw.url,
318
612
  });
319
613
  }
614
+ const factsFlag = data.flagBootstrap?.["ai.facts.enabled"];
615
+ recordFactsFlagBootstrapOutcome(factsFlag === true
616
+ ? "enabled"
617
+ : factsFlag === false
618
+ ? "disabled"
619
+ : "invalid");
320
620
  return {
321
621
  agentType: organization.agentType,
322
622
  agents,
623
+ controlPlaneUrl: this.baseUrl(),
624
+ isFactsEnabled: factsFlag === true,
323
625
  };
324
626
  }
325
627
  async createApplication(input) {
@@ -384,6 +686,7 @@ export class HttpSuperblocksServerClient {
384
686
  return await this.requestData(`${path}${query}`, {
385
687
  apiKey: input.apiKey,
386
688
  method: "GET",
689
+ operation: "live_edit",
387
690
  retry: false,
388
691
  timeoutMs: Math.max(1, deadlineAtMs - Date.now()),
389
692
  });
@@ -502,12 +805,48 @@ export class HttpSuperblocksServerClient {
502
805
  if (input.applicationId) {
503
806
  params.set("applicationId", input.applicationId);
504
807
  }
505
- const data = await this.requestData(`/api/v1/integrations/superset?${params.toString()}`, { apiKey: input.apiKey, method: "GET" });
808
+ const data = await this.requestData(`/api/v1/integrations/superset?${params.toString()}`, {
809
+ apiKey: input.apiKey,
810
+ method: "GET",
811
+ operation: "superset_integrations",
812
+ });
506
813
  return Array.isArray(data) ? data : [];
507
814
  }
508
815
  async getIntegration(input) {
509
816
  try {
510
- return await this.requestData(`/api/v1/integrations/${encodeURIComponent(input.integrationId)}`, { apiKey: input.apiKey, method: "GET" });
817
+ return await this.requestData(`/api/v1/integrations/${encodeURIComponent(input.integrationId)}`, { apiKey: input.apiKey, method: "GET", operation: "integration" });
818
+ }
819
+ catch (error) {
820
+ if (error instanceof ServerHttpError && error.status === 404) {
821
+ return undefined;
822
+ }
823
+ throw error;
824
+ }
825
+ }
826
+ async getApplicationEnvironment(input) {
827
+ try {
828
+ const data = await this.requestData(`/api/v2/applications/${encodeURIComponent(input.applicationId)}/environments`, { apiKey: input.apiKey, method: "GET" });
829
+ const editor = data?.profiles?.editor;
830
+ const availableProfileIds = Array.isArray(editor?.availableProfileIds)
831
+ ? editor.availableProfileIds.filter((id) => typeof id === "string")
832
+ : undefined;
833
+ return {
834
+ ...(typeof data?.needsAttention?.editor === "boolean"
835
+ ? { needsAttention: { editor: data.needsAttention.editor } }
836
+ : {}),
837
+ profiles: {
838
+ ...(availableProfileIds
839
+ ? {
840
+ editor: {
841
+ availableProfileIds,
842
+ ...(typeof editor?.defaultProfileId === "string"
843
+ ? { defaultProfileId: editor.defaultProfileId }
844
+ : {}),
845
+ },
846
+ }
847
+ : {}),
848
+ },
849
+ };
511
850
  }
512
851
  catch (error) {
513
852
  if (error instanceof ServerHttpError && error.status === 404) {
@@ -517,7 +856,11 @@ export class HttpSuperblocksServerClient {
517
856
  }
518
857
  }
519
858
  async getOrganizationProfiles(input) {
520
- const data = await this.requestData(`/api/v1/organizations/${encodeURIComponent(input.organizationId)}/profiles`, { apiKey: input.apiKey, method: "GET" });
859
+ const data = await this.requestData(`/api/v1/organizations/${encodeURIComponent(input.organizationId)}/profiles`, {
860
+ apiKey: input.apiKey,
861
+ method: "GET",
862
+ operation: "organization_profiles",
863
+ });
521
864
  if (!Array.isArray(data)) {
522
865
  return [];
523
866
  }
@@ -527,6 +870,9 @@ export class HttpSuperblocksServerClient {
527
870
  ...(row.allowedInEditor !== undefined
528
871
  ? { allowedInEditor: row.allowedInEditor }
529
872
  : {}),
873
+ ...(typeof row.defaultInEditor === "boolean"
874
+ ? { defaultInEditor: row.defaultInEditor }
875
+ : {}),
530
876
  ...(row.displayName ? { displayName: row.displayName } : {}),
531
877
  ...(row.id ? { id: row.id } : {}),
532
878
  key: row.key,
@@ -585,6 +931,8 @@ export class HttpSuperblocksServerClient {
585
931
  apiKey: input.apiKey,
586
932
  body: JSON.stringify({ directoryHashes: [input.directoryHash] }),
587
933
  method: "POST",
934
+ operation: "build_progress",
935
+ retry: true,
588
936
  ...(input.signal ? { signal: input.signal } : {}),
589
937
  ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
590
938
  });
@@ -669,7 +1017,11 @@ export class HttpSuperblocksServerClient {
669
1017
  * why nothing on a hot path should call it.
670
1018
  */
671
1019
  async getApplicationName(input) {
672
- const body = await this.request("/api/v2/applications", { apiKey: input.apiKey, method: "GET" });
1020
+ const body = await this.request("/api/v2/applications", {
1021
+ apiKey: input.apiKey,
1022
+ method: "GET",
1023
+ operation: "applications",
1024
+ });
673
1025
  const found = body?.data?.applications?.find((application) => application?.id === input.applicationId);
674
1026
  return found?.name?.trim() || undefined;
675
1027
  }