@brainbase-labs/cli 0.58.0 → 0.59.0-rc.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 (2) hide show
  1. package/dist/index.js +2822 -677
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -36091,7 +36091,7 @@ function padStart(s, n) {
36091
36091
  // package.json
36092
36092
  var package_default = {
36093
36093
  name: "@brainbase-labs/cli",
36094
- version: "0.58.0",
36094
+ version: "0.59.0-rc.1",
36095
36095
  description: "Pack, share, and install agent templates across harnesses (Claude Code, Codex, ...).",
36096
36096
  type: "module",
36097
36097
  bin: {
@@ -40975,6 +40975,245 @@ import fs7 from "node:fs";
40975
40975
  import path8 from "node:path";
40976
40976
  import fs6 from "node:fs";
40977
40977
  import { randomUUID } from "node:crypto";
40978
+
40979
+ // src/core/http-retry.ts
40980
+ var HTTP_RETRY_DEADLINE_MS = 30000;
40981
+ var HTTP_RETRY_MAX_ATTEMPTS = 3;
40982
+ var DEFAULT_RETRY_BUDGET_MS = 300000;
40983
+ var IDLE_WATCHDOG_MS = 45000;
40984
+ var REFRESH_RETRY_WINDOW_MS = 8000;
40985
+ var FULL_JITTER_BASE_MS = 100;
40986
+ var FULL_JITTER_FACTOR = 2;
40987
+ var FULL_JITTER_MAX_MS = 30000;
40988
+ var STREAM_RETRY_MAX_DELAY_MS = 30000;
40989
+ function httpRetryEnabled(env = process.env) {
40990
+ return env.BRAINBASE_HTTP_RETRY?.trim() !== "0";
40991
+ }
40992
+ function parseRetryAfterMs(header, nowMs = Date.now()) {
40993
+ const raw = header?.trim();
40994
+ if (!raw)
40995
+ return null;
40996
+ if (/^\d+$/.test(raw)) {
40997
+ const delayMs = Number(raw) * 1000;
40998
+ return Number.isFinite(delayMs) ? delayMs : null;
40999
+ }
41000
+ const weekday = "(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)";
41001
+ const month = "(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)";
41002
+ const time = "\\d{2}:\\d{2}:\\d{2}";
41003
+ const httpDateForms = [
41004
+ `${weekday}, \\d{2} ${month} \\d{4} ${time} GMT`,
41005
+ `(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), \\d{2}-${month}-\\d{2} ${time} GMT`,
41006
+ `${weekday} ${month} (?: \\d|\\d{2}) ${time} \\d{4}`
41007
+ ];
41008
+ if (!httpDateForms.some((form) => new RegExp(`^${form}$`).test(raw)))
41009
+ return null;
41010
+ const date = Date.parse(raw.endsWith("GMT") ? raw : `${raw} GMT`);
41011
+ if (Number.isNaN(date))
41012
+ return null;
41013
+ return Math.max(0, date - nowMs);
41014
+ }
41015
+ function fullJitterDelayMs(attempt, opts = {}) {
41016
+ const { baseMs = FULL_JITTER_BASE_MS, factor = FULL_JITTER_FACTOR, maxMs = FULL_JITTER_MAX_MS } = opts;
41017
+ const draw = opts.rng ?? Math.random;
41018
+ const exponent = Math.max(0, Math.floor(attempt));
41019
+ const cap = Math.min(maxMs, baseMs * factor ** exponent);
41020
+ return Math.floor(draw() * cap);
41021
+ }
41022
+ function createRetryBudget(totalMs, startedAtMs = Date.now()) {
41023
+ const deadlineMs = startedAtMs + totalMs;
41024
+ return {
41025
+ totalMs,
41026
+ remainingMs(nowMs = Date.now()) {
41027
+ return Math.max(0, deadlineMs - nowMs);
41028
+ },
41029
+ fits(delayMs, nowMs = Date.now()) {
41030
+ return delayMs < this.remainingMs(nowMs);
41031
+ }
41032
+ };
41033
+ }
41034
+ function retryDelayMs(attempt, budget, retryAfterHeader, opts = {}, nowMs = Date.now()) {
41035
+ const parsed = parseRetryAfterMs(retryAfterHeader, nowMs);
41036
+ const delayMs = parsed ?? fullJitterDelayMs(attempt, opts);
41037
+ return budget.fits(delayMs, nowMs) ? delayMs : null;
41038
+ }
41039
+ var UNSENT_CODES = new Set([
41040
+ "ConnectionRefused",
41041
+ "ECONNREFUSED",
41042
+ "ENOTFOUND",
41043
+ "EAI_AGAIN",
41044
+ "FailedToOpenSocket",
41045
+ "UND_ERR_CONNECT_TIMEOUT",
41046
+ "UND_ERR_POOL_TIMEOUT",
41047
+ "UND_ERR_QUEUE_TIMEOUT",
41048
+ "ConnectError",
41049
+ "CERT_HAS_EXPIRED",
41050
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
41051
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
41052
+ "ERR_TLS_CERT_ALTNAME_INVALID"
41053
+ ]);
41054
+ var AMBIGUOUS_CODES = new Set([
41055
+ "ECONNRESET",
41056
+ "EPIPE",
41057
+ "UND_ERR_SOCKET",
41058
+ "UND_ERR_BODY_TIMEOUT",
41059
+ "UND_ERR_HEADERS_TIMEOUT",
41060
+ "SocketError"
41061
+ ]);
41062
+ function networkFailureCode(error) {
41063
+ const seen = new Set;
41064
+ while (error && typeof error === "object" && !seen.has(error)) {
41065
+ seen.add(error);
41066
+ const record = error;
41067
+ if (typeof record.code === "string")
41068
+ return record.code;
41069
+ error = record.cause;
41070
+ }
41071
+ return;
41072
+ }
41073
+ function isHttpTimeout(error) {
41074
+ const seen = new Set;
41075
+ while (error && typeof error === "object" && !seen.has(error)) {
41076
+ seen.add(error);
41077
+ const record = error;
41078
+ if (record.name === "TimeoutError" || record.name === "TimeoutDOMException" || record.code === "request_timeout" || record.code === "UND_ERR_BODY_TIMEOUT" || record.code === "UND_ERR_HEADERS_TIMEOUT")
41079
+ return true;
41080
+ error = record.cause;
41081
+ }
41082
+ return false;
41083
+ }
41084
+ function classifyHttpFailure(error, keyed = false) {
41085
+ if (!error || typeof error !== "object")
41086
+ return "permanent";
41087
+ const record = error;
41088
+ const code = record.body?.code ?? record.body?.detail?.code ?? record.body?.error?.code ?? record.code;
41089
+ if (code === "invalid_grant" || code === "idempotency_key_reused" || code === "idempotency_resource_deleted")
41090
+ return "permanent";
41091
+ const networkCode = networkFailureCode(record.cause) ?? networkFailureCode(error);
41092
+ if (networkCode === "ERR_INVALID_URL" || record.cause?.message === "bad port")
41093
+ return "permanent";
41094
+ if (record.definitelyUnsent === true)
41095
+ return "unsent";
41096
+ if (["ConnectError", "ConnectTimeoutError", "PoolTimeoutError"].includes(record.name ?? ""))
41097
+ return "unsent";
41098
+ if (record.definitelyUnsent === false)
41099
+ return "ambiguous";
41100
+ if (UNSENT_CODES.has(networkCode ?? ""))
41101
+ return "unsent";
41102
+ if (AMBIGUOUS_CODES.has(networkCode ?? "") || isHttpTimeout(error))
41103
+ return "ambiguous";
41104
+ if (record.status === 409)
41105
+ return keyed && code === "idempotency_request_in_progress" ? "retryable_status" : "permanent";
41106
+ if ([408, 425, 429, 500, 502, 503, 504, 520, 521, 522, 523, 524].includes(record.status ?? 0))
41107
+ return "retryable_status";
41108
+ if (/^(PGRST00[0-3]|08\w*|53300|57P0\w)$/.test(code ?? ""))
41109
+ return "retryable_status";
41110
+ if (record.name === "TypeError" && /fetch failed|terminated|socket|disconnected/i.test(record.message ?? ""))
41111
+ return "ambiguous";
41112
+ return "permanent";
41113
+ }
41114
+ function retrySleep(ms, signal) {
41115
+ return new Promise((resolve, reject) => {
41116
+ if (signal?.aborted) {
41117
+ reject(signal.reason);
41118
+ return;
41119
+ }
41120
+ const abort = () => {
41121
+ clearTimeout(timer);
41122
+ reject(signal?.reason);
41123
+ };
41124
+ const timer = setTimeout(() => {
41125
+ signal?.removeEventListener("abort", abort);
41126
+ resolve();
41127
+ }, ms);
41128
+ signal?.addEventListener("abort", abort, { once: true });
41129
+ });
41130
+ }
41131
+ function logRetryEvent(event) {
41132
+ const logger = {
41133
+ warn: console.warn,
41134
+ error: console.error,
41135
+ info: (record) => {
41136
+ process.stderr.write(`${record}
41137
+ `);
41138
+ }
41139
+ };
41140
+ const log = event.event === "retry_attempt" ? logger.warn : event.event === "retry_exhausted" ? logger.error : logger.info;
41141
+ log(JSON.stringify(event));
41142
+ }
41143
+ async function retryHttpRequest(operation, options) {
41144
+ if (!httpRetryEnabled())
41145
+ return operation(options.signal, 1);
41146
+ const now = options.now ?? Date.now;
41147
+ const started = now();
41148
+ const budget = createRetryBudget(options.budgetMs ?? HTTP_RETRY_DEADLINE_MS, started);
41149
+ const emit = options.onRetryEvent ?? logRetryEvent;
41150
+ const sleep = options.sleep ?? retrySleep;
41151
+ const maxAttempts = options.maxAttempts ?? HTTP_RETRY_MAX_ATTEMPTS;
41152
+ let lastError;
41153
+ for (let attempt = 1;; attempt++) {
41154
+ if (options.signal?.aborted)
41155
+ throw options.signal.reason;
41156
+ const remaining = budget.remainingMs(now());
41157
+ if (remaining <= 0)
41158
+ throw lastError ?? new DOMException("HTTP retry budget exhausted", "TimeoutError");
41159
+ const controller = new AbortController;
41160
+ const abort = () => controller.abort(options.signal?.reason);
41161
+ options.signal?.addEventListener("abort", abort, { once: true });
41162
+ const timeout = setTimeout(() => controller.abort(new DOMException("HTTP request deadline exceeded", "TimeoutError")), Math.max(1, Math.floor(Math.min(remaining, options.timeoutMs ?? remaining))));
41163
+ let rejectAbort;
41164
+ try {
41165
+ const aborted = new Promise((_resolve, reject) => {
41166
+ rejectAbort = () => reject(controller.signal.reason);
41167
+ controller.signal.addEventListener("abort", rejectAbort, { once: true });
41168
+ });
41169
+ const result = await Promise.race([operation(controller.signal, attempt), aborted]);
41170
+ if (budget.remainingMs(now()) <= 0)
41171
+ throw new DOMException("HTTP request deadline exceeded", "TimeoutError");
41172
+ return result;
41173
+ } catch (error) {
41174
+ lastError = error;
41175
+ } finally {
41176
+ clearTimeout(timeout);
41177
+ options.signal?.removeEventListener("abort", abort);
41178
+ if (rejectAbort)
41179
+ controller.signal.removeEventListener("abort", rejectAbort);
41180
+ }
41181
+ if (options.signal?.aborted)
41182
+ throw options.signal.reason;
41183
+ const failureClass = classifyHttpFailure(lastError, options.keyed);
41184
+ const attemptsUsed = Math.max(attempt, options.attemptsUsed?.() ?? attempt);
41185
+ const record = lastError;
41186
+ const event = {
41187
+ layer: options.layer ?? "http",
41188
+ op: options.op ?? "request",
41189
+ attempt: attemptsUsed,
41190
+ class: failureClass,
41191
+ status: record?.status ?? null,
41192
+ elapsed_ms: Math.max(0, Math.floor(now() - started)),
41193
+ budget_ms: budget.totalMs,
41194
+ error: record?.name ?? "Error"
41195
+ };
41196
+ const retryable = failureClass === "unsent" || (failureClass === "ambiguous" || failureClass === "retryable_status") && (options.idempotent || options.keyed);
41197
+ if (!retryable || failureClass === "ambiguous" && options.retryTimeouts === false && isHttpTimeout(lastError)) {
41198
+ if (failureClass !== "permanent")
41199
+ emit({ ...event, event: "retry_deferred" });
41200
+ throw lastError;
41201
+ }
41202
+ const delay = retryDelayMs(attemptsUsed - 1, budget, record?.retryAfter, { rng: options.rng }, now());
41203
+ if (attemptsUsed >= maxAttempts || delay === null) {
41204
+ emit({ ...event, event: "retry_exhausted" });
41205
+ throw lastError;
41206
+ }
41207
+ emit({ ...event, event: "retry_attempt", delay_ms: delay });
41208
+ await sleep(delay, options.signal);
41209
+ if (budget.remainingMs(now()) <= 0) {
41210
+ emit({ ...event, event: "retry_exhausted", elapsed_ms: Math.max(0, Math.floor(now() - started)) });
41211
+ throw lastError;
41212
+ }
41213
+ }
41214
+ }
41215
+
41216
+ // src/core/auth.ts
40978
41217
  var AUTH_FILE = path8.join(BRAINBASE_HOME, "auth.json");
40979
41218
  var AUTH_LOCK_FILE = `${AUTH_FILE}.lock`;
40980
41219
  var AUTH_LOCK_TIMEOUT_MS = 5000;
@@ -41038,15 +41277,15 @@ function acquireLock(lockFile, timeoutMs) {
41038
41277
  }
41039
41278
  }
41040
41279
  }
41041
- function acquireAuthLock() {
41042
- const release = acquireLock(AUTH_LOCK_FILE, AUTH_LOCK_TIMEOUT_MS);
41280
+ function acquireAuthLock(timeoutMs = AUTH_LOCK_TIMEOUT_MS) {
41281
+ const release = acquireLock(AUTH_LOCK_FILE, timeoutMs);
41043
41282
  if (!release) {
41044
41283
  throw new Error(`Timed out waiting to update CLI authentication.${lockHolderState(AUTH_LOCK_FILE)} Retry, or remove ${AUTH_LOCK_FILE}`);
41045
41284
  }
41046
41285
  return release;
41047
41286
  }
41048
- function withAuthLock(operation) {
41049
- const release = acquireAuthLock();
41287
+ function withAuthLock(operation, timeoutMs = AUTH_LOCK_TIMEOUT_MS) {
41288
+ const release = acquireAuthLock(timeoutMs);
41050
41289
  try {
41051
41290
  return operation();
41052
41291
  } finally {
@@ -41183,6 +41422,163 @@ function markRefreshRejected(session, code) {
41183
41422
  });
41184
41423
  });
41185
41424
  }
41425
+ function isUnsentAuthRequest(cause) {
41426
+ const seen = new Set;
41427
+ while (cause && typeof cause === "object" && !seen.has(cause)) {
41428
+ seen.add(cause);
41429
+ const value = cause;
41430
+ if (value.code && [
41431
+ "ConnectionRefused",
41432
+ "ECONNREFUSED",
41433
+ "ENOTFOUND",
41434
+ "EAI_AGAIN",
41435
+ "FailedToOpenSocket",
41436
+ "UND_ERR_CONNECT_TIMEOUT"
41437
+ ].includes(value.code))
41438
+ return true;
41439
+ cause = value.cause;
41440
+ }
41441
+ return false;
41442
+ }
41443
+ async function exchangeRefresh(session, timeoutMs) {
41444
+ let res;
41445
+ try {
41446
+ res = await fetch(`${session.supabase_url.replace(/\/+$/, "")}/auth/v1/token?grant_type=refresh_token`, {
41447
+ method: "POST",
41448
+ signal: AbortSignal.timeout(Math.max(1, Math.floor(timeoutMs))),
41449
+ headers: {
41450
+ "Content-Type": "application/json",
41451
+ apikey: session.supabase_anon_key,
41452
+ Authorization: `Bearer ${session.supabase_anon_key}`
41453
+ },
41454
+ body: JSON.stringify({ refresh_token: session.refresh_token })
41455
+ });
41456
+ } catch (cause) {
41457
+ return {
41458
+ kind: "transient",
41459
+ failure: {
41460
+ kind: "transient",
41461
+ class: isUnsentAuthRequest(cause) ? "unsent" : "ambiguous",
41462
+ status: null,
41463
+ detail: "couldn't reach the auth server",
41464
+ cause
41465
+ }
41466
+ };
41467
+ }
41468
+ if (!res.ok) {
41469
+ const code = await rejectionCode(res);
41470
+ if (code)
41471
+ return { kind: "failure", failure: { kind: "rejected", detail: code } };
41472
+ if ([408, 425, 429, 500, 502, 503, 504, 520, 521, 522, 523, 524].includes(res.status)) {
41473
+ await res.body?.cancel().catch(() => {});
41474
+ return {
41475
+ kind: "transient",
41476
+ failure: {
41477
+ kind: "transient",
41478
+ class: "retryable_status",
41479
+ status: res.status,
41480
+ retryAfter: res.headers.get("Retry-After"),
41481
+ detail: "couldn't reach the auth server"
41482
+ }
41483
+ };
41484
+ }
41485
+ return {
41486
+ kind: "failure",
41487
+ failure: {
41488
+ kind: "unavailable",
41489
+ detail: `the auth server answered HTTP ${res.status}`
41490
+ }
41491
+ };
41492
+ }
41493
+ try {
41494
+ const data = await res.json();
41495
+ if (!data || typeof data.access_token !== "string" || !data.access_token) {
41496
+ throw new Error("Missing access token");
41497
+ }
41498
+ return { kind: "success", data };
41499
+ } catch (cause) {
41500
+ return {
41501
+ kind: "transient",
41502
+ failure: {
41503
+ kind: "transient",
41504
+ class: "ambiguous",
41505
+ status: res.status,
41506
+ detail: "couldn't reach the auth server",
41507
+ cause
41508
+ }
41509
+ };
41510
+ }
41511
+ }
41512
+ function saveRefreshResult(session, data) {
41513
+ const now = Math.floor(Date.now() / 1000);
41514
+ const updated = {
41515
+ ...session,
41516
+ access_token: data.access_token,
41517
+ refresh_token: data.refresh_token ?? session.refresh_token,
41518
+ expires_at: data.expires_at ?? (data.expires_in ? now + data.expires_in : undefined),
41519
+ user_id: data.user?.id ?? session.user_id,
41520
+ email: data.user?.email ?? session.email,
41521
+ authedAt: new Date().toISOString()
41522
+ };
41523
+ if (!isSameAuthIdentity(updated, session))
41524
+ throw new AuthSessionChangedError;
41525
+ return withAuthLock(() => {
41526
+ const adopted = adoptRefreshedLineage(session);
41527
+ if (adopted !== undefined)
41528
+ return adopted;
41529
+ writeAuthUnlocked(updated);
41530
+ lastRefreshFailure = null;
41531
+ return updated;
41532
+ });
41533
+ }
41534
+ async function refreshWithRetry(session) {
41535
+ const started = Date.now();
41536
+ const budget = createRetryBudget(REFRESH_RETRY_WINDOW_MS, started);
41537
+ for (let attempt = 1;attempt <= HTTP_RETRY_MAX_ATTEMPTS; attempt++) {
41538
+ const adopted = adoptRefreshedLineage(session);
41539
+ if (adopted !== undefined)
41540
+ return adopted;
41541
+ const current = readAuth();
41542
+ if (current.refresh_rejected)
41543
+ return null;
41544
+ if (!budget.remainingMs())
41545
+ break;
41546
+ const result = await exchangeRefresh(session, Math.min(REFRESH_REQUEST_TIMEOUT_MS, budget.remainingMs()));
41547
+ if (result.kind === "success") {
41548
+ return saveRefreshResult(session, result.data);
41549
+ }
41550
+ const latest = adoptRefreshedLineage(session);
41551
+ if (latest !== undefined)
41552
+ return latest;
41553
+ noteRefreshFailure(session, result.failure);
41554
+ if (result.kind === "failure") {
41555
+ if (result.failure.kind === "rejected") {
41556
+ markRefreshRejected(session, result.failure.detail);
41557
+ }
41558
+ return null;
41559
+ }
41560
+ const delay = attempt < HTTP_RETRY_MAX_ATTEMPTS ? retryDelayMs(attempt - 1, budget, result.failure.retryAfter) : null;
41561
+ const event = {
41562
+ event: delay === null ? "retry_exhausted" : "retry_attempt",
41563
+ layer: "supabase",
41564
+ op: "refresh_session",
41565
+ attempt,
41566
+ class: result.failure.class,
41567
+ status: result.failure.status,
41568
+ elapsed_ms: Date.now() - started,
41569
+ budget_ms: REFRESH_RETRY_WINDOW_MS,
41570
+ error: result.failure.cause instanceof Error ? result.failure.cause.name : "HttpError",
41571
+ ...delay === null ? {} : { delay_ms: delay }
41572
+ };
41573
+ if (delay === null) {
41574
+ console.error(JSON.stringify(event));
41575
+ return null;
41576
+ }
41577
+ console.warn(JSON.stringify(event));
41578
+ await new Promise((resolve) => setTimeout(resolve, delay));
41579
+ }
41580
+ return null;
41581
+ }
41186
41582
  var refreshInFlight = null;
41187
41583
  function validatedRefreshResult(expectedSession, result) {
41188
41584
  const current = readAuth();
@@ -41234,6 +41630,8 @@ async function refreshSession(session) {
41234
41630
  });
41235
41631
  return null;
41236
41632
  }
41633
+ if (httpRetryEnabled())
41634
+ return await refreshWithRetry(session);
41237
41635
  const url = `${session.supabase_url.replace(/\/+$/, "")}/auth/v1/token?grant_type=refresh_token`;
41238
41636
  let res;
41239
41637
  try {
@@ -41284,28 +41682,7 @@ async function refreshSession(session) {
41284
41682
  });
41285
41683
  return null;
41286
41684
  }
41287
- const now = Math.floor(Date.now() / 1000);
41288
- const expires_at = data.expires_at ?? (data.expires_in ? now + data.expires_in : undefined);
41289
- const updated = {
41290
- ...session,
41291
- access_token: data.access_token,
41292
- refresh_token: data.refresh_token ?? session.refresh_token,
41293
- expires_at,
41294
- user_id: data.user?.id ?? session.user_id,
41295
- email: data.user?.email ?? session.email,
41296
- authedAt: new Date().toISOString()
41297
- };
41298
- if (!isSameAuthIdentity(updated, session)) {
41299
- throw new AuthSessionChangedError;
41300
- }
41301
- return withAuthLock(() => {
41302
- const adopted2 = adoptRefreshedLineage(session);
41303
- if (adopted2 !== undefined)
41304
- return adopted2;
41305
- writeAuthUnlocked(updated);
41306
- lastRefreshFailure = null;
41307
- return updated;
41308
- });
41685
+ return saveRefreshResult(session, data);
41309
41686
  } finally {
41310
41687
  releaseRefreshLock();
41311
41688
  }
@@ -41356,7 +41733,9 @@ async function refreshSessionAfterUnauthorized(rejectedSession) {
41356
41733
  if (latest.access_token !== rejectedSession.access_token && isAuthValid(latest)) {
41357
41734
  return latest;
41358
41735
  }
41359
- expireRejectedAccessToken(rejectedSession);
41736
+ if (!httpRetryEnabled() || refreshFailureFor(rejectedSession)?.kind !== "transient") {
41737
+ expireRejectedAccessToken(rejectedSession);
41738
+ }
41360
41739
  return null;
41361
41740
  }
41362
41741
  function expireRejectedAccessToken(session) {
@@ -55061,10 +55440,14 @@ function announceStoredPatFallback(session, stored) {
55061
55440
  if (!session || announcedFallback)
55062
55441
  return;
55063
55442
  announcedFallback = true;
55064
- console.error(`Login session ${sessionProblem(session)}; using the PAT saved in ${TOKEN_FILE} ` + `(${stored.prefix}…). Run \`brainbase login\` to switch back.`);
55443
+ const nextStep = refreshFailureFor(session)?.kind === "transient" ? "Retry when the auth server is reachable to use your login session again." : "Run `brainbase login` to switch back.";
55444
+ console.error(`Login session ${sessionProblem(session)}; using the PAT saved in ${TOKEN_FILE} ` + `(${stored.prefix}…). ${nextStep}`);
55065
55445
  }
55066
55446
  function sessionProblem(session) {
55067
55447
  const failure = refreshFailureFor(session);
55448
+ if (failure?.kind === "transient") {
55449
+ return "couldn't reach the auth server to renew it";
55450
+ }
55068
55451
  if (failure?.kind === "rejected") {
55069
55452
  return `expired and could not be renewed (the auth server rejected its refresh token: ${failure.detail})`;
55070
55453
  }
@@ -55082,6 +55465,9 @@ function noUsableCredentialMessage() {
55082
55465
  return "Not logged in. Run `brainbase login` to connect this device.";
55083
55466
  }
55084
55467
  const failure = refreshFailureFor(session);
55468
+ if (failure?.kind === "transient") {
55469
+ return "We couldn't reach the auth server to renew your login session. Check your connection and retry.";
55470
+ }
55085
55471
  const retry = failure?.kind === "unavailable" ? " Check your connection and retry, or run `brainbase login`." : " Run `brainbase login` to sign in again.";
55086
55472
  const stored = readToken();
55087
55473
  const mismatch = stored && !storedPatFor(session) ? ` The PAT saved in ${TOKEN_FILE} belongs to a different account, so it was not used.` : "";
@@ -55093,6 +55479,9 @@ function withRejectedSessionHint(message, source) {
55093
55479
  const trimmed = message.trimEnd();
55094
55480
  const separator = /[.!?:]$/.test(trimmed) ? " " : ". ";
55095
55481
  const session = readAuth();
55482
+ if (session && refreshFailureFor(session)?.kind === "transient") {
55483
+ return `${trimmed}${separator}We couldn't reach the auth server to renew your login session. Check your connection and retry.`;
55484
+ }
55096
55485
  const renewable = session ? isSessionRefreshable(session) : false;
55097
55486
  if (session && !renewable && storedPatFor(session)) {
55098
55487
  return `${trimmed}${separator}The login session could not be renewed; ` + `retry to use the PAT saved in ${TOKEN_FILE}, or run \`brainbase login\`.`;
@@ -55139,7 +55528,7 @@ var DEFAULT_PROXY_BASE = "https://api.v1.brainbaselabs.com";
55139
55528
  var MAS_IN_PROGRESS_MAX_WAIT_MS = 15000;
55140
55529
  var MAS_REQUEST_TIMEOUT_MS = 30000;
55141
55530
  var MAS_MIN_RETRY_DELAY_MS = 100;
55142
- var MAS_MAX_RETRY_DELAY_MS = 2000;
55531
+ var AUTH_REFRESH_RESERVE_MS = 1e4 + REFRESH_RETRY_WINDOW_MS + 5000;
55143
55532
  var DEFINITELY_UNSENT_NETWORK_CODES = new Set([
55144
55533
  "ConnectionRefused",
55145
55534
  "ECONNREFUSED",
@@ -55156,8 +55545,8 @@ class ApiError extends Error {
55156
55545
  status;
55157
55546
  body;
55158
55547
  retryAfter;
55159
- constructor(message, status, body) {
55160
- super(message);
55548
+ constructor(message, status, body, cause) {
55549
+ super(message, cause === undefined ? undefined : { cause });
55161
55550
  this.status = status;
55162
55551
  this.body = body;
55163
55552
  this.name = "ApiError";
@@ -55166,13 +55555,13 @@ class ApiError extends Error {
55166
55555
 
55167
55556
  class NetworkApiError extends ApiError {
55168
55557
  definitelyUnsent;
55169
- constructor(message, definitelyUnsent) {
55170
- super(message);
55558
+ constructor(message, definitelyUnsent, cause) {
55559
+ super(message, undefined, undefined, httpRetryEnabled() ? cause : undefined);
55171
55560
  this.definitelyUnsent = definitelyUnsent;
55172
55561
  }
55173
55562
  }
55174
55563
  function isLostResponseError(error) {
55175
- return error instanceof NetworkApiError && !error.definitelyUnsent;
55564
+ return error instanceof ApiError && (error.outcomeUnknown === true || error instanceof NetworkApiError && !error.definitelyUnsent);
55176
55565
  }
55177
55566
  function isUnsentRequestError(error) {
55178
55567
  return error instanceof NetworkApiError && error.definitelyUnsent;
@@ -55275,6 +55664,8 @@ function networkErrorCode(error) {
55275
55664
  return typeof causeCode === "string" ? causeCode : undefined;
55276
55665
  }
55277
55666
  function isDefinitelyUnsentNetworkError(error) {
55667
+ if (httpRetryEnabled())
55668
+ return classifyHttpFailure(error) === "unsent";
55278
55669
  const code = networkErrorCode(error);
55279
55670
  if (DEFINITELY_UNSENT_NETWORK_CODES.has(code ?? ""))
55280
55671
  return true;
@@ -55296,7 +55687,7 @@ async function sendRequest(url, init, bearer) {
55296
55687
  try {
55297
55688
  return await fetch(url, { ...init, headers });
55298
55689
  } catch (err) {
55299
- throw new NetworkApiError(`Network error: ${err.message}`, isDefinitelyUnsentNetworkError(err));
55690
+ throw new NetworkApiError(`Network error: ${err.message}`, isDefinitelyUnsentNetworkError(err), err);
55300
55691
  }
55301
55692
  }
55302
55693
  async function sendWithAuthRetry(session, send) {
@@ -55311,7 +55702,106 @@ async function sendWithAuthRetry(session, send) {
55311
55702
  res.body.cancel().catch(() => {});
55312
55703
  return await send(refreshed);
55313
55704
  }
55314
- async function request(pathname, init = {}) {
55705
+ function controlPlaneApiErrorMessage(body, status) {
55706
+ const detail = body && typeof body === "object" ? body.detail : undefined;
55707
+ return detail && ["self_edge_disabled", "runtime_auto_disabled"].includes(detail.code ?? "") && typeof detail.message === "string" ? `${detail.code}: ${detail.message}` : apiErrorMessage(body, status);
55708
+ }
55709
+ async function resilientApiRequest(pathname, init, resource, onHeaders, options = {}) {
55710
+ const credential = await resolveCredential();
55711
+ let currentSession = credential.session;
55712
+ let refreshAvailable = credential.source === "session" && !new Headers(init.headers).has("Authorization");
55713
+ const method2 = (init.method ?? "GET").toUpperCase();
55714
+ const keyed = !!new Headers(init.headers).get("Idempotency-Key")?.trim();
55715
+ const singleAttempt = options.singleAttempt === true || resource && method2 === "DELETE" && !keyed || options.timeoutMs !== undefined;
55716
+ let sends = 0;
55717
+ let lastResponse;
55718
+ let ambiguousWrite = false;
55719
+ const read = method2 === "GET" || method2 === "HEAD";
55720
+ const budget = createRetryBudget(options.timeoutMs ?? HTTP_RETRY_DEADLINE_MS);
55721
+ try {
55722
+ return await retryHttpRequest(async (signal) => {
55723
+ const send = async (refreshed) => {
55724
+ if (refreshed) {
55725
+ currentSession = refreshed;
55726
+ refreshAvailable = false;
55727
+ }
55728
+ if (signal?.aborted)
55729
+ throw signal.reason;
55730
+ if (sends >= HTTP_RETRY_MAX_ATTEMPTS)
55731
+ throw new ApiError("HTTP request attempt budget exhausted", lastResponse?.status);
55732
+ sends++;
55733
+ try {
55734
+ lastResponse = await sendRequest(`${resource ? masApiBase(currentSession) : apiBase(currentSession)}${pathname}`, { ...init, signal }, currentSession?.access_token ?? credential.bearer);
55735
+ } catch (error) {
55736
+ if (!read && isLostResponseError(error))
55737
+ ambiguousWrite = true;
55738
+ throw error;
55739
+ }
55740
+ return lastResponse;
55741
+ };
55742
+ const first = await send();
55743
+ const res = first.status === 401 && refreshAvailable && !signal?.aborted && sends < HTTP_RETRY_MAX_ATTEMPTS && budget.fits(AUTH_REFRESH_RESERVE_MS) ? await sendWithAuthRetry(currentSession, (refreshed) => refreshed ? send(refreshed) : Promise.resolve(first)) : first;
55744
+ let text2;
55745
+ try {
55746
+ text2 = await res.text();
55747
+ } catch (error) {
55748
+ if (resource && read && !res.ok && !signal?.aborted) {
55749
+ const message = `MAS returned HTTP ${res.status}, but its response body was interrupted`;
55750
+ const failure = new ApiError(res.status === 401 ? withRejectedSessionHint(message, credential.source) : message, res.status);
55751
+ failure.retryAfter = res.headers.get("Retry-After") ?? undefined;
55752
+ throw failure;
55753
+ }
55754
+ if (!read)
55755
+ ambiguousWrite = true;
55756
+ throw new NetworkApiError(`Network error while reading response: ${error.message}`, false, error);
55757
+ }
55758
+ let body = text2;
55759
+ try {
55760
+ body = text2 ? JSON.parse(text2) : null;
55761
+ } catch {}
55762
+ if (!res.ok) {
55763
+ const message = resource ? masApiErrorMessage(body, res.status) : controlPlaneApiErrorMessage(body, res.status);
55764
+ const failure = new ApiError(res.status === 401 ? withRejectedSessionHint(message, credential.source) : resource && res.status === 404 && isUnroutedPath(body) ? `This control plane does not serve ${pathname} yet. Update the server, or use the web app.` : message, res.status, body);
55765
+ failure.retryAfter = res.headers.get("Retry-After") ?? undefined;
55766
+ if (!read && res.status !== 429 && res.status !== 425 && classifyHttpFailure(failure, keyed) === "retryable_status")
55767
+ ambiguousWrite = true;
55768
+ throw failure;
55769
+ }
55770
+ onHeaders?.(res.headers);
55771
+ return body;
55772
+ }, {
55773
+ idempotent: read || options.idempotent === true,
55774
+ keyed,
55775
+ signal: init.signal ?? undefined,
55776
+ budgetMs: options.timeoutMs ?? HTTP_RETRY_DEADLINE_MS,
55777
+ maxAttempts: singleAttempt ? 1 : HTTP_RETRY_MAX_ATTEMPTS,
55778
+ attemptsUsed: () => sends,
55779
+ retryTimeouts: false,
55780
+ onRetryEvent: options.singleAttempt ? () => {} : undefined,
55781
+ op: options.op ?? (resource ? `${method2} /v2` : `${method2} /v2/cli`)
55782
+ });
55783
+ } catch (error) {
55784
+ if (init.signal?.aborted)
55785
+ throw init.signal.reason;
55786
+ if (ambiguousWrite && isUnsentRequestError(error)) {
55787
+ throw new NetworkApiError(error.message, false, error);
55788
+ }
55789
+ if (ambiguousWrite && error instanceof ApiError) {
55790
+ const body = error.body;
55791
+ const code = body?.code ?? body?.detail?.code ?? body?.error?.code;
55792
+ const settled = error.status === 409 && (code === "idempotency_key_reused" || code === "idempotency_resource_deleted");
55793
+ if (!settled)
55794
+ error.outcomeUnknown = true;
55795
+ }
55796
+ if (isHttpTimeout(error) && !(error instanceof NetworkApiError)) {
55797
+ throw new NetworkApiError(`Network error: ${error.message}`, false, error);
55798
+ }
55799
+ throw error;
55800
+ }
55801
+ }
55802
+ async function request(pathname, init = {}, options = { idempotent: false }) {
55803
+ if (httpRetryEnabled())
55804
+ return resilientApiRequest(pathname, init, false, undefined, options);
55315
55805
  const credential = await resolveCredential();
55316
55806
  let currentSession = credential.session;
55317
55807
  let refreshSessionAvailable = credential.source === "session" && !new Headers(init.headers).has("Authorization");
@@ -55332,7 +55822,7 @@ async function request(pathname, init = {}) {
55332
55822
  try {
55333
55823
  text2 = await res.text();
55334
55824
  } catch (error) {
55335
- throw new NetworkApiError(`Network error while reading response: ${error.message}`, false);
55825
+ throw new NetworkApiError(`Network error while reading response: ${error.message}`, false, error);
55336
55826
  }
55337
55827
  break;
55338
55828
  } catch (error) {
@@ -55347,8 +55837,7 @@ async function request(pathname, init = {}) {
55347
55837
  body = text2 ? JSON.parse(text2) : null;
55348
55838
  } catch {}
55349
55839
  if (!res.ok) {
55350
- const detail = body && typeof body === "object" ? body.detail : undefined;
55351
- const message = detail && ["self_edge_disabled", "runtime_auto_disabled"].includes(detail.code ?? "") && typeof detail.message === "string" ? `${detail.code}: ${detail.message}` : apiErrorMessage(body, res.status);
55840
+ const message = controlPlaneApiErrorMessage(body, res.status);
55352
55841
  throw new ApiError(res.status === 401 ? withRejectedSessionHint(message, credential.source) : message, res.status, body);
55353
55842
  }
55354
55843
  return body;
@@ -55384,7 +55873,7 @@ function masRetryDelayMs(retryAfterHeader, fallbackMs) {
55384
55873
  if (!Number.isFinite(retryAfterSeconds) || retryAfterSeconds < 0) {
55385
55874
  return fallbackMs;
55386
55875
  }
55387
- return Math.min(MAS_MAX_RETRY_DELAY_MS, Math.max(MAS_MIN_RETRY_DELAY_MS, retryAfterSeconds * 1000));
55876
+ return Math.min(2000, Math.max(MAS_MIN_RETRY_DELAY_MS, retryAfterSeconds * 1000));
55388
55877
  }
55389
55878
  function parseMasTaskCreateResponse(body) {
55390
55879
  if (!body || typeof body !== "object") {
@@ -55402,6 +55891,21 @@ function parseMasTaskCreateResponse(body) {
55402
55891
  };
55403
55892
  }
55404
55893
  async function masRequest(pathname, init) {
55894
+ if (httpRetryEnabled()) {
55895
+ try {
55896
+ return await resilientApiRequest(pathname, init, true);
55897
+ } catch (error) {
55898
+ const failure = error;
55899
+ const detail = failure.body?.detail;
55900
+ if (detail?.code === "idempotency_request_in_progress") {
55901
+ throw new ApiError(detail.task_id ? `Task ${detail.task_id} is still being processed. Check its status before starting another task.` : failure.message, failure.status, failure.body, error);
55902
+ }
55903
+ if (isLostResponseError(error) || failure.status !== undefined && failure.status >= 500) {
55904
+ throw new ApiError(`${failure.message} Task creation may still be processing; check your tasks before running this command again.`, failure.status, failure.body, error);
55905
+ }
55906
+ throw error;
55907
+ }
55908
+ }
55405
55909
  const credential = await resolveCredential();
55406
55910
  let currentSession = credential.session;
55407
55911
  let ambiguousNetworkRetriesRemaining = 1;
@@ -55506,7 +56010,9 @@ function masNamedRecord(body, what, nameField) {
55506
56010
  }
55507
56011
  return { id: record.id, name };
55508
56012
  }
55509
- async function masReadRequest(pathname, callerSignal) {
56013
+ async function masReadRequest(pathname, callerSignal, options = {}) {
56014
+ if (httpRetryEnabled())
56015
+ return resilientApiRequest(pathname, { signal: callerSignal }, true, undefined, options);
55510
56016
  const credential = await resolveCredential();
55511
56017
  let currentSession = credential.session;
55512
56018
  let refreshSessionAvailable = credential.source === "session";
@@ -55529,7 +56035,7 @@ async function masReadRequest(pathname, callerSignal) {
55529
56035
  try {
55530
56036
  text2 = await res.text();
55531
56037
  } catch (error) {
55532
- throw new NetworkApiError(`Network error while reading response: ${error.message}`, false);
56038
+ throw new NetworkApiError(`Network error while reading response: ${error.message}`, false, error);
55533
56039
  }
55534
56040
  break;
55535
56041
  } catch (error) {
@@ -55545,7 +56051,9 @@ async function masReadRequest(pathname, callerSignal) {
55545
56051
  } catch {}
55546
56052
  if (!res.ok) {
55547
56053
  const message = masApiErrorMessage(body, res.status);
55548
- throw new ApiError(res.status === 401 ? withRejectedSessionHint(message, credential.source) : res.status === 404 && isUnroutedPath(body) ? `This control plane does not serve ${pathname} yet. Update the server, or use the web app.` : message, res.status, body);
56054
+ const error = new ApiError(res.status === 401 ? withRejectedSessionHint(message, credential.source) : res.status === 404 && isUnroutedPath(body) ? `This control plane does not serve ${pathname} yet. Update the server, or use the web app.` : message, res.status, body);
56055
+ error.retryAfter = res.headers.get("retry-after") ?? undefined;
56056
+ throw error;
55549
56057
  }
55550
56058
  return body;
55551
56059
  }
@@ -55564,6 +56072,8 @@ function withRequestDeadline(callerSignal, timeoutMs) {
55564
56072
  return combined.signal;
55565
56073
  }
55566
56074
  async function masResourceRequest(pathname, init = {}, onHeaders, options = {}) {
56075
+ if (httpRetryEnabled())
56076
+ return resilientApiRequest(pathname, init, true, onHeaders, options);
55567
56077
  const credential = await resolveCredential();
55568
56078
  let currentSession = credential.session;
55569
56079
  const res = await sendWithAuthRetry(credential.source === "session" ? currentSession : null, async (refreshed) => {
@@ -55578,7 +56088,7 @@ async function masResourceRequest(pathname, init = {}, onHeaders, options = {})
55578
56088
  try {
55579
56089
  text2 = await res.text();
55580
56090
  } catch (error) {
55581
- throw new NetworkApiError(`Network error while reading response: ${error.message}`, false);
56091
+ throw new NetworkApiError(`Network error while reading response: ${error.message}`, false, error);
55582
56092
  }
55583
56093
  let body = text2;
55584
56094
  try {
@@ -55603,8 +56113,12 @@ async function masStreamTarget(pathname) {
55603
56113
  const base2 = masApiBase(credential.session);
55604
56114
  return {
55605
56115
  url: `${base2}${pathname}`,
55606
- resolveBearer: async () => {
55607
- const fresh = await resolveCredential();
56116
+ resolveBearer: async (rejectedBearer) => {
56117
+ let fresh = await resolveCredential();
56118
+ if (rejectedBearer && fresh.source === "session" && fresh.session?.access_token === rejectedBearer) {
56119
+ await refreshSessionAfterUnauthorized(fresh.session);
56120
+ fresh = await resolveCredential();
56121
+ }
55608
56122
  const freshBase = masApiBase(fresh.session);
55609
56123
  if (freshBase !== base2) {
55610
56124
  throw new StreamHostMovedError(`The control plane moved from ${base2} to ${freshBase} while this stream was open, so the current credential was not sent. Re-run the command to follow against the new host.`);
@@ -55701,7 +56215,7 @@ var masApi = {
55701
56215
  return masItems(await masReadRequest(`/tasks${qs}`), "task list");
55702
56216
  },
55703
56217
  async getTask(taskId, options = {}) {
55704
- const body = await masReadRequest(`/tasks/${encodeURIComponent(taskId)}`, options.signal);
56218
+ const body = await masReadRequest(`/tasks/${encodeURIComponent(taskId)}`, options.signal, { singleAttempt: options.singleAttempt });
55705
56219
  return parseMasTask(body);
55706
56220
  },
55707
56221
  async listTaskEvents(taskId, options = {}) {
@@ -55714,7 +56228,7 @@ var masApi = {
55714
56228
  }
55715
56229
  if (options.desc !== undefined)
55716
56230
  params.set("desc", String(options.desc));
55717
- const body = await masReadRequest(`/tasks/${encodeURIComponent(taskId)}/events?${params.toString()}`);
56231
+ const body = await masReadRequest(`/tasks/${encodeURIComponent(taskId)}/events?${params.toString()}`, options.signal);
55718
56232
  return masItems(body, "task events");
55719
56233
  },
55720
56234
  async listMachines(options = {}) {
@@ -55899,26 +56413,29 @@ var api = {
55899
56413
  body: JSON.stringify(input)
55900
56414
  });
55901
56415
  },
55902
- getAgent(agentId) {
55903
- return request(`/agents/${encodeURIComponent(agentId)}`);
56416
+ getAgent(agentId, options) {
56417
+ return request(`/agents/${encodeURIComponent(agentId)}`, options);
55904
56418
  },
55905
56419
  getAgentRevision(agentId) {
55906
56420
  return request(`/agents/${encodeURIComponent(agentId)}/revision`);
55907
56421
  },
55908
- getAgentManifest(agentId) {
55909
- return request(`/agents/${encodeURIComponent(agentId)}/manifest`);
56422
+ getAgentManifest(agentId, options) {
56423
+ return request(`/agents/${encodeURIComponent(agentId)}/manifest`, options);
55910
56424
  },
55911
- updateAgent(agentId, input) {
56425
+ updateAgent(agentId, input, options) {
55912
56426
  if (usesLegacyControlPlane() && (input.machine_kind !== undefined || input.default_model !== undefined || input.runtime_mode !== undefined)) {
55913
56427
  return Promise.reject(legacyAgentConfigError());
55914
56428
  }
56429
+ const { idempotent = false, ...transport } = options ?? {};
55915
56430
  return request(`/agents/${encodeURIComponent(agentId)}`, {
56431
+ ...transport,
55916
56432
  method: "PUT",
55917
56433
  body: JSON.stringify(input)
55918
- });
56434
+ }, { idempotent, op: idempotent ? "update_agent" : undefined });
55919
56435
  },
55920
- pushAgentManifest(agentId, input) {
56436
+ pushAgentManifest(agentId, input, options) {
55921
56437
  return request(`/agents/${encodeURIComponent(agentId)}/manifest`, {
56438
+ ...options,
55922
56439
  method: "PUT",
55923
56440
  body: JSON.stringify(input)
55924
56441
  });
@@ -55946,11 +56463,13 @@ var api = {
55946
56463
  }
55947
56464
  return { secrets };
55948
56465
  },
55949
- putAgentSecrets(agentId, secrets) {
56466
+ putAgentSecrets(agentId, secrets, options) {
56467
+ const { idempotent = false, ...transport } = options ?? {};
55950
56468
  return request(`/agents/${encodeURIComponent(agentId)}/secrets`, {
56469
+ ...transport,
55951
56470
  method: "PUT",
55952
56471
  body: JSON.stringify({ secrets })
55953
- });
56472
+ }, { idempotent, op: idempotent ? "put_agent_secrets" : undefined });
55954
56473
  },
55955
56474
  createCliKey(agentId, input) {
55956
56475
  return request(`/agents/${encodeURIComponent(agentId)}/keys`, {
@@ -56002,8 +56521,8 @@ var api = {
56002
56521
  body: JSON.stringify(input)
56003
56522
  });
56004
56523
  },
56005
- getOrchestration(orchId) {
56006
- return request(`/orchestrations/${encodeURIComponent(orchId)}`);
56524
+ getOrchestration(orchId, options) {
56525
+ return request(`/orchestrations/${encodeURIComponent(orchId)}`, options);
56007
56526
  },
56008
56527
  fireOrchestrationTrigger(orchId, triggerId, payload, idempotencyKey) {
56009
56528
  return masResourceRequest(`/orchestrations/${encodeURIComponent(orchId)}/triggers/${encodeURIComponent(triggerId)}/fire`, {
@@ -56015,7 +56534,7 @@ var api = {
56015
56534
  getOrchestrationManifest(orchId) {
56016
56535
  return request(`/orchestrations/${encodeURIComponent(orchId)}/manifest`);
56017
56536
  },
56018
- async updateOrchestration(orchId, input) {
56537
+ async updateOrchestration(orchId, input, options) {
56019
56538
  let payload = input;
56020
56539
  if (usesLegacyControlPlane() && input.triggers !== undefined) {
56021
56540
  if (input.triggers.length > 0)
@@ -56028,6 +56547,7 @@ var api = {
56028
56547
  payload = legacyPayload;
56029
56548
  }
56030
56549
  return request(`/orchestrations/${encodeURIComponent(orchId)}`, {
56550
+ ...options,
56031
56551
  method: "PUT",
56032
56552
  body: JSON.stringify(payload)
56033
56553
  });
@@ -56046,6 +56566,7 @@ function proxyBaseUrl(session) {
56046
56566
  }
56047
56567
 
56048
56568
  // src/core/registry-client.ts
56569
+ import { setTimeout as sleep } from "node:timers/promises";
56049
56570
  var DEFAULT_BASE = "https://api.v1.brainbaselabs.com";
56050
56571
  function baseUrl(session) {
56051
56572
  const env3 = process.env.BRAINBASE_REGISTRY_URL || process.env.BRAINBASE_API_URL;
@@ -56120,27 +56641,256 @@ function mergeHeaders(auth, overrides) {
56120
56641
  new Headers(overrides).forEach((value, key2) => headers.set(key2, value));
56121
56642
  return headers;
56122
56643
  }
56123
- async function sendRequest2(pathname, init, session) {
56644
+ var RETRYABLE_GET_STATUSES = new Set([
56645
+ 408,
56646
+ 425,
56647
+ 429,
56648
+ 500,
56649
+ 502,
56650
+ 503,
56651
+ 504,
56652
+ 520,
56653
+ 521,
56654
+ 522,
56655
+ 523,
56656
+ 524
56657
+ ]);
56658
+ var AUTH_REFRESH_RESERVE_MS2 = 1e4 + REFRESH_RETRY_WINDOW_MS + 5000;
56659
+ var DEFINITELY_UNSENT_NETWORK_CODES2 = new Set([
56660
+ "ConnectionRefused",
56661
+ "ECONNREFUSED",
56662
+ "ENOTFOUND",
56663
+ "EAI_AGAIN",
56664
+ "ERR_INVALID_URL",
56665
+ "FailedToOpenSocket",
56666
+ "UND_ERR_CONNECT_TIMEOUT"
56667
+ ]);
56668
+ function networkErrorCode2(error) {
56669
+ if (!error || typeof error !== "object")
56670
+ return;
56671
+ const code = error.code;
56672
+ if (typeof code === "string")
56673
+ return code;
56674
+ const cause = error.cause;
56675
+ if (!cause || typeof cause !== "object")
56676
+ return;
56677
+ const causeCode = cause.code;
56678
+ return typeof causeCode === "string" ? causeCode : undefined;
56679
+ }
56680
+ function isDefinitelyUnsentNetworkError2(error) {
56681
+ return DEFINITELY_UNSENT_NETWORK_CODES2.has(networkErrorCode2(error) ?? "");
56682
+ }
56683
+
56684
+ class RegistryLocalBodyError extends Error {
56685
+ original;
56686
+ constructor(original) {
56687
+ super(original instanceof Error ? original.message : String(original));
56688
+ this.original = original;
56689
+ this.name = "RegistryLocalBodyError";
56690
+ }
56691
+ }
56692
+ function toNetworkApiError(err) {
56693
+ const apiError = new ApiError(`Network error: ${err.message}`);
56694
+ apiError.cause = err;
56695
+ return apiError;
56696
+ }
56697
+ function attemptSignal(external2, timeoutMs) {
56698
+ const controller = new AbortController;
56699
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
56700
+ const onAbort = () => controller.abort(external2?.reason);
56701
+ if (external2?.aborted)
56702
+ onAbort();
56703
+ else
56704
+ external2?.addEventListener("abort", onAbort, { once: true });
56705
+ return {
56706
+ signal: controller.signal,
56707
+ cleanup: () => {
56708
+ clearTimeout(timer);
56709
+ external2?.removeEventListener("abort", onAbort);
56710
+ }
56711
+ };
56712
+ }
56713
+ function responseWithCleanup(res, cleanup) {
56714
+ const reader = res.body.getReader();
56715
+ reader.closed.then(cleanup, cleanup);
56716
+ const body = new ReadableStream({
56717
+ async pull(controller) {
56718
+ try {
56719
+ const { value, done } = await reader.read();
56720
+ if (done)
56721
+ controller.close();
56722
+ else
56723
+ controller.enqueue(value);
56724
+ } catch (error) {
56725
+ controller.error(error);
56726
+ }
56727
+ },
56728
+ async cancel(reason) {
56729
+ try {
56730
+ await reader.cancel(reason);
56731
+ } finally {
56732
+ cleanup();
56733
+ }
56734
+ }
56735
+ });
56736
+ const wrapped = new Response(body, {
56737
+ status: res.status,
56738
+ statusText: res.statusText,
56739
+ headers: res.headers
56740
+ });
56741
+ Object.defineProperties(wrapped, {
56742
+ url: { value: res.url },
56743
+ redirected: { value: res.redirected },
56744
+ type: { value: res.type }
56745
+ });
56746
+ return wrapped;
56747
+ }
56748
+ async function consumeBody(res, opts) {
56749
+ if (!opts.readBody || !res.body)
56750
+ return;
56751
+ try {
56752
+ await opts.readBody(res);
56753
+ } catch (err) {
56754
+ if (res.ok)
56755
+ throw err;
56756
+ const refusal = new ApiError(`HTTP ${res.status}`, res.status);
56757
+ refusal.cause = err;
56758
+ throw refusal;
56759
+ }
56760
+ }
56761
+ function logRetry(state, failureClass, status, error, delayMs) {
56762
+ const record = {
56763
+ event: delayMs === null ? "retry_exhausted" : "retry_attempt",
56764
+ layer: "http",
56765
+ op: "registry_get",
56766
+ attempt: state.attempts,
56767
+ class: failureClass,
56768
+ status,
56769
+ elapsed_ms: state.budget.totalMs - state.budget.remainingMs(),
56770
+ budget_ms: state.budget.totalMs,
56771
+ error,
56772
+ ...delayMs === null ? {} : { delay_ms: delayMs }
56773
+ };
56774
+ if (delayMs === null)
56775
+ console.error(JSON.stringify(record));
56776
+ else
56777
+ console.warn(JSON.stringify(record));
56778
+ }
56779
+ async function retryingGetRequest(url, init, opts, state) {
56780
+ const { budget, signal } = state;
56781
+ let lastNetworkError = null;
56782
+ while (state.attempts < HTTP_RETRY_MAX_ATTEMPTS && budget.remainingMs() > 0) {
56783
+ state.attempts += 1;
56784
+ try {
56785
+ const res = await fetch(url, { ...init, signal });
56786
+ if (RETRYABLE_GET_STATUSES.has(res.status)) {
56787
+ const delayMs = state.attempts < HTTP_RETRY_MAX_ATTEMPTS ? retryDelayMs(state.attempts - 1, budget, res.headers.get("Retry-After")) : null;
56788
+ logRetry(state, "retryable_status", res.status, "ApiError", delayMs);
56789
+ if (delayMs !== null) {
56790
+ if (res.body)
56791
+ res.body.cancel().catch(() => {});
56792
+ await sleep(delayMs, undefined, { signal });
56793
+ continue;
56794
+ }
56795
+ }
56796
+ if (res.status !== 401)
56797
+ await consumeBody(res, opts);
56798
+ return res;
56799
+ } catch (err) {
56800
+ if (err instanceof RegistryLocalBodyError)
56801
+ throw err;
56802
+ if (err instanceof ApiError && err.status !== undefined)
56803
+ throw err;
56804
+ if (init.signal?.aborted)
56805
+ throw toNetworkApiError(err);
56806
+ lastNetworkError = toNetworkApiError(err);
56807
+ const delayMs = state.attempts < HTTP_RETRY_MAX_ATTEMPTS && !signal.aborted ? retryDelayMs(state.attempts - 1, budget, null) : null;
56808
+ logRetry(state, isDefinitelyUnsentNetworkError2(err) ? "unsent" : "ambiguous", null, err instanceof Error ? err.name : "Error", delayMs);
56809
+ if (delayMs === null)
56810
+ break;
56811
+ try {
56812
+ await sleep(delayMs, undefined, { signal });
56813
+ } catch (abort) {
56814
+ throw toNetworkApiError(abort);
56815
+ }
56816
+ }
56817
+ }
56818
+ throw lastNetworkError ?? new ApiError("Network error: request timed out");
56819
+ }
56820
+ async function sendRequest2(pathname, init, session, opts = {}, state) {
56124
56821
  const url = `${baseUrl(session)}${pathname}`;
56822
+ if (!state) {
56823
+ try {
56824
+ return await fetch(url, init);
56825
+ } catch (err) {
56826
+ throw new ApiError(`Network error: ${err.message}`);
56827
+ }
56828
+ }
56829
+ state.signal.throwIfAborted();
56830
+ if ((init.method ?? "GET").toUpperCase() === "GET") {
56831
+ return await retryingGetRequest(url, init, opts, state);
56832
+ }
56125
56833
  try {
56126
- return await fetch(url, init);
56834
+ const res = await fetch(url, { ...init, signal: state.signal });
56835
+ if (res.status !== 401)
56836
+ await consumeBody(res, opts);
56837
+ return res;
56127
56838
  } catch (err) {
56128
- throw new ApiError(`Network error: ${err.message}`);
56839
+ if (err instanceof ApiError)
56840
+ throw err;
56841
+ throw toNetworkApiError(err);
56129
56842
  }
56130
56843
  }
56131
- async function sendAuthenticatedRequest(pathname, init, auth) {
56844
+ async function sendAuthenticatedRequest(pathname, init, auth, opts = {}, state) {
56132
56845
  return await sendRequest2(pathname, {
56133
56846
  ...init,
56134
56847
  headers: mergeHeaders(auth.headers, init.headers)
56135
- }, auth.session);
56848
+ }, auth.session, opts, state);
56849
+ }
56850
+ async function registryFetch(pathname, init = {}, opts = {}) {
56851
+ if (httpRetryEnabled()) {
56852
+ const budget = createRetryBudget(HTTP_RETRY_DEADLINE_MS);
56853
+ const { signal, cleanup } = attemptSignal(init.signal, budget.remainingMs());
56854
+ const state = { budget, attempts: 0, signal };
56855
+ let bodyOwnsCleanup = false;
56856
+ try {
56857
+ const res = await fetchWithAuth(pathname, init, opts, state);
56858
+ if (!res.bodyUsed)
56859
+ await consumeBody(res, opts);
56860
+ if (!opts.readBody && res.body && !res.bodyUsed) {
56861
+ const wrapped = responseWithCleanup(res, cleanup);
56862
+ bodyOwnsCleanup = true;
56863
+ return wrapped;
56864
+ }
56865
+ return res;
56866
+ } catch (err) {
56867
+ if (err instanceof RegistryLocalBodyError)
56868
+ throw err.original;
56869
+ if (err instanceof ApiError)
56870
+ throw err;
56871
+ throw toNetworkApiError(err);
56872
+ } finally {
56873
+ if (!bodyOwnsCleanup)
56874
+ cleanup();
56875
+ }
56876
+ }
56877
+ return await fetchWithAuth(pathname, init);
56136
56878
  }
56137
- async function registryFetch(pathname, init = {}) {
56879
+ async function fetchWithAuth(pathname, init, opts = {}, state) {
56880
+ state?.signal.throwIfAborted();
56138
56881
  if (new Headers(init.headers).has("Authorization")) {
56139
- return await sendRequest2(pathname, init, readAuth());
56882
+ return await sendRequest2(pathname, init, readAuth(), opts, state);
56140
56883
  }
56141
56884
  const auth = await resolveAuth();
56142
56885
  const retrySession = auth.headers["X-Auth-Source"] === "jwt" ? auth.session : null;
56143
- return await sendWithAuthRetry(retrySession, (refreshed) => sendAuthenticatedRequest(pathname, init, refreshed ? jwtAuth(refreshed) : auth));
56886
+ const send = (refreshed) => sendAuthenticatedRequest(pathname, init, refreshed ? jwtAuth(refreshed) : auth, opts, state);
56887
+ if (!state)
56888
+ return await sendWithAuthRetry(retrySession, send);
56889
+ const res = await send();
56890
+ if (res.status !== 401 || !retrySession || state.signal.aborted || state.attempts >= HTTP_RETRY_MAX_ATTEMPTS || !state.budget.fits(AUTH_REFRESH_RESERVE_MS2)) {
56891
+ return res;
56892
+ }
56893
+ return await sendWithAuthRetry(retrySession, (refreshed) => refreshed ? send(refreshed) : Promise.resolve(res));
56144
56894
  }
56145
56895
  async function jsonRequest(pathname, init = {}) {
56146
56896
  const headers = new Headers(init.headers);
@@ -56149,8 +56899,15 @@ async function jsonRequest(pathname, init = {}) {
56149
56899
  if (init.body && !headers.has("Content-Type") && typeof init.body === "string") {
56150
56900
  headers.set("Content-Type", "application/json");
56151
56901
  }
56152
- const res = await registryFetch(pathname, { ...init, headers });
56153
- const text2 = await res.text();
56902
+ let bodyText = null;
56903
+ let bodyFrom = null;
56904
+ const res = await registryFetch(pathname, { ...init, headers }, {
56905
+ readBody: async (response) => {
56906
+ bodyText = await response.text();
56907
+ bodyFrom = response;
56908
+ }
56909
+ });
56910
+ const text2 = bodyFrom === res && bodyText !== null ? bodyText : await res.text();
56154
56911
  let body = text2;
56155
56912
  try {
56156
56913
  body = text2 ? JSON.parse(text2) : null;
@@ -56208,32 +56965,58 @@ var registryApi = {
56208
56965
  },
56209
56966
  async getFile(creator, slug, version, relPath) {
56210
56967
  const pathname = `/v1/registry/templates/${encodeURIComponent(creator)}/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}/files/${relPath}`;
56211
- const res = await registryFetch(pathname);
56968
+ let buffer = null;
56969
+ let bufferFrom = null;
56970
+ const res = await registryFetch(pathname, {}, {
56971
+ readBody: async (response) => {
56972
+ if (!response.ok)
56973
+ return;
56974
+ buffer = Buffer.from(await response.arrayBuffer());
56975
+ bufferFrom = response;
56976
+ }
56977
+ });
56212
56978
  if (!res.ok) {
56213
56979
  throw new ApiError(`HTTP ${res.status} reading file ${relPath}`, res.status);
56214
56980
  }
56981
+ if (bufferFrom === res && buffer !== null)
56982
+ return buffer;
56215
56983
  const ab = await res.arrayBuffer();
56216
56984
  return Buffer.from(ab);
56217
56985
  },
56218
56986
  async downloadTarball(creator, slug, version, destPath) {
56219
56987
  const pathname = `/v1/registry/templates/${encodeURIComponent(creator)}/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}/tarball`;
56220
- const res = await registryFetch(pathname);
56221
- if (!res.ok || !res.body) {
56222
- throw new ApiError(`HTTP ${res.status} downloading tarball`, res.status);
56223
- }
56224
- const out = fs54.createWriteStream(destPath);
56225
- const reader = res.body.getReader();
56226
- try {
56227
- while (true) {
56228
- const { value, done } = await reader.read();
56229
- if (done)
56230
- break;
56231
- if (value) {
56232
- await new Promise((resolve, reject2) => out.write(value, (err) => err ? reject2(err) : resolve()));
56988
+ if (!httpRetryEnabled()) {
56989
+ const res2 = await registryFetch(pathname);
56990
+ if (!res2.ok || !res2.body) {
56991
+ throw new ApiError(`HTTP ${res2.status} downloading tarball`, res2.status);
56992
+ }
56993
+ const out = fs54.createWriteStream(destPath);
56994
+ const reader = res2.body.getReader();
56995
+ try {
56996
+ while (true) {
56997
+ const { value, done } = await reader.read();
56998
+ if (done)
56999
+ break;
57000
+ if (value) {
57001
+ await new Promise((resolve, reject2) => out.write(value, (err) => err ? reject2(err) : resolve()));
57002
+ }
56233
57003
  }
57004
+ } finally {
57005
+ out.end();
56234
57006
  }
56235
- } finally {
56236
- out.end();
57007
+ return;
57008
+ }
57009
+ let downloadedFrom = null;
57010
+ const res = await registryFetch(pathname, {}, {
57011
+ readBody: async (response) => {
57012
+ if (!response.ok || !response.body)
57013
+ return;
57014
+ await streamResponseToFile(response.body, destPath);
57015
+ downloadedFrom = response;
57016
+ }
57017
+ });
57018
+ if (!res.ok || !res.body || downloadedFrom !== res) {
57019
+ throw new ApiError(`HTTP ${res.status} downloading tarball`, res.status);
56237
57020
  }
56238
57021
  },
56239
57022
  createPackage(input) {
@@ -56251,20 +57034,24 @@ var registryApi = {
56251
57034
  form.set("manifest", new Blob([input.manifestJson], { type: "application/json" }), "manifest.json");
56252
57035
  const bytes = fs54.readFileSync(input.bundlePath);
56253
57036
  form.set("bundle", new Blob([bytes], { type: "application/gzip" }), "bundle.tgz");
56254
- const res = await registryFetch(pathname, {
57037
+ const init = {
56255
57038
  method: "POST",
56256
57039
  headers: { Accept: "application/json" },
56257
57040
  body: form
56258
- });
56259
- const text2 = await res.text();
56260
- let body = text2;
56261
- try {
56262
- body = text2 ? JSON.parse(text2) : null;
56263
- } catch {}
56264
- if (!res.ok) {
56265
- throw new ApiError(`Publish failed: HTTP ${res.status}`, res.status, body);
57041
+ };
57042
+ if (!httpRetryEnabled()) {
57043
+ const res = await registryFetch(pathname, init);
57044
+ const text2 = await res.text();
57045
+ let body = text2;
57046
+ try {
57047
+ body = text2 ? JSON.parse(text2) : null;
57048
+ } catch {}
57049
+ if (!res.ok) {
57050
+ throw new ApiError(`Publish failed: HTTP ${res.status}`, res.status, body);
57051
+ }
57052
+ return body;
56266
57053
  }
56267
- return body;
57054
+ return await publishVersionVerified(creator, slug, input, pathname, init);
56268
57055
  },
56269
57056
  yankVersion(creator, slug, version, reason) {
56270
57057
  return jsonRequest(`/v1/registry/templates/${encodeURIComponent(creator)}/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}/yank`, {
@@ -56297,6 +57084,97 @@ var registryApi = {
56297
57084
  return jsonRequest(`/v1/registry/cli-tokens/${encodeURIComponent(id)}`, { method: "DELETE" });
56298
57085
  }
56299
57086
  };
57087
+ async function readBackPublishedVersion(creator, slug, version, bundleSha256) {
57088
+ try {
57089
+ const readBack = await registryApi.getVersion(creator, slug, version);
57090
+ if (readBack.version === version && readBack.bundle_sha256 === bundleSha256) {
57091
+ return readBack;
57092
+ }
57093
+ } catch {}
57094
+ return null;
57095
+ }
57096
+ async function publishVersionVerified(creator, slug, input, pathname, init) {
57097
+ let res;
57098
+ let text2 = "";
57099
+ try {
57100
+ res = await registryFetch(pathname, init, {
57101
+ readBody: async (response) => {
57102
+ text2 = await response.text();
57103
+ }
57104
+ });
57105
+ } catch (err) {
57106
+ if (!(err instanceof ApiError) || err.status !== undefined || isDefinitelyUnsentNetworkError2(err.cause)) {
57107
+ throw err;
57108
+ }
57109
+ const adopted = await readBackPublishedVersion(creator, slug, input.version, input.bundleSha256);
57110
+ if (adopted)
57111
+ return adopted;
57112
+ const failure = new ApiError(`Publish status undetermined: ${err.message}; could not confirm ${input.version} with the expected bundle checksum. Check the registry before publishing again.`);
57113
+ failure.cause = err;
57114
+ throw failure;
57115
+ }
57116
+ let body = text2;
57117
+ try {
57118
+ body = text2 ? JSON.parse(text2) : null;
57119
+ } catch {}
57120
+ if (!res.ok) {
57121
+ throw new ApiError(`Publish failed: HTTP ${res.status}`, res.status, body);
57122
+ }
57123
+ let readBack;
57124
+ try {
57125
+ readBack = await registryApi.getVersion(creator, slug, input.version);
57126
+ } catch (err) {
57127
+ const failure = new ApiError(`Publish status undetermined: version ${input.version} was submitted but reading it back failed (${err.message}). Check the registry before publishing again.`);
57128
+ failure.cause = err;
57129
+ throw failure;
57130
+ }
57131
+ if (readBack.version !== input.version || readBack.bundle_sha256 !== input.bundleSha256) {
57132
+ throw new ApiError(`Publish verification failed: reading ${input.version} back returned version ${readBack.version} with bundle_sha256 ${readBack.bundle_sha256}, expected ${input.version} with ${input.bundleSha256}.`);
57133
+ }
57134
+ return readBack;
57135
+ }
57136
+ async function streamResponseToFile(body, destPath) {
57137
+ let out;
57138
+ try {
57139
+ out = fs54.createWriteStream(destPath);
57140
+ } catch (err) {
57141
+ throw new RegistryLocalBodyError(err);
57142
+ }
57143
+ const closed = new Promise((resolve) => out.once("close", () => resolve()));
57144
+ let writeError = null;
57145
+ out.on("error", (err) => {
57146
+ writeError ??= err;
57147
+ });
57148
+ const reader = body.getReader();
57149
+ let failure = null;
57150
+ try {
57151
+ while (true) {
57152
+ const { value, done } = await reader.read();
57153
+ if (done)
57154
+ break;
57155
+ if (value) {
57156
+ try {
57157
+ await new Promise((resolve, reject2) => out.write(value, (err) => err ? reject2(err) : resolve()));
57158
+ } catch (err) {
57159
+ throw new RegistryLocalBodyError(err);
57160
+ }
57161
+ }
57162
+ }
57163
+ } catch (err) {
57164
+ failure = err;
57165
+ reader.cancel().catch(() => {});
57166
+ }
57167
+ if (failure === null) {
57168
+ out.end();
57169
+ } else {
57170
+ out.destroy();
57171
+ }
57172
+ await closed;
57173
+ if (failure !== null)
57174
+ throw failure;
57175
+ if (writeError !== null)
57176
+ throw new RegistryLocalBodyError(writeError);
57177
+ }
56300
57178
 
56301
57179
  // src/core/tarball.ts
56302
57180
  import fs57 from "node:fs";
@@ -63055,6 +63933,12 @@ var DEFAULT_POLL_INTERVAL_MS = 2000;
63055
63933
  var MIN_POLL_INTERVAL_MS = 500;
63056
63934
  var REQUEST_TIMEOUT_MS = 1e4;
63057
63935
  var MAX_CONSECUTIVE_FAILURES = 20;
63936
+ var retryDiagnostics = {
63937
+ info(message) {
63938
+ process.stderr.write(`${message}
63939
+ `);
63940
+ }
63941
+ };
63058
63942
 
63059
63943
  class LoginTimeoutError extends Error {
63060
63944
  constructor(timeoutMs, lastError) {
@@ -63062,7 +63946,7 @@ class LoginTimeoutError extends Error {
63062
63946
  this.name = "LoginTimeoutError";
63063
63947
  }
63064
63948
  }
63065
- function sleep(ms2) {
63949
+ function sleep2(ms2) {
63066
63950
  return new Promise((resolve) => setTimeout(resolve, ms2));
63067
63951
  }
63068
63952
  var MAX_POLL_INTERVAL_MS = 60000;
@@ -63112,6 +63996,49 @@ async function readJson2(res) {
63112
63996
  return null;
63113
63997
  }
63114
63998
  }
63999
+ function interruptedLogin() {
64000
+ return new Error("The auth server sent an unreadable or interrupted login response. Run `brainbase login` again.");
64001
+ }
64002
+ async function startWithRetry(url, fetchImpl) {
64003
+ const started = Date.now();
64004
+ const budget = createRetryBudget(HTTP_RETRY_DEADLINE_MS, started);
64005
+ for (let attempt2 = 1;; attempt2++) {
64006
+ try {
64007
+ return await fetchImpl(url, {
64008
+ method: "POST",
64009
+ headers: JSON_HEADERS,
64010
+ body: "{}",
64011
+ signal: AbortSignal.timeout(Math.max(1, Math.floor(Math.min(REQUEST_TIMEOUT_MS, budget.remainingMs()))))
64012
+ });
64013
+ } catch (cause) {
64014
+ const unsent = isUnsentAuthRequest(cause);
64015
+ const delay2 = unsent && attempt2 < HTTP_RETRY_MAX_ATTEMPTS ? retryDelayMs(attempt2 - 1, budget) : null;
64016
+ const event = {
64017
+ event: delay2 === null ? unsent ? "retry_exhausted" : "retry_deferred" : "retry_attempt",
64018
+ layer: "http",
64019
+ op: "device_authorization_start",
64020
+ attempt: attempt2,
64021
+ class: unsent ? "unsent" : "ambiguous",
64022
+ status: null,
64023
+ elapsed_ms: Date.now() - started,
64024
+ budget_ms: HTTP_RETRY_DEADLINE_MS,
64025
+ error: cause instanceof Error ? cause.name : "Error",
64026
+ ...delay2 === null ? {} : { delay_ms: delay2 }
64027
+ };
64028
+ if (delay2 === null) {
64029
+ if (unsent)
64030
+ console.error(JSON.stringify(event));
64031
+ else
64032
+ retryDiagnostics.info(JSON.stringify(event));
64033
+ throw new Error(`Could not reach ${url}: ${cause.message}`, {
64034
+ cause
64035
+ });
64036
+ }
64037
+ console.warn(JSON.stringify(event));
64038
+ await sleep2(delay2);
64039
+ }
64040
+ }
64041
+ }
63115
64042
  var JSON_HEADERS = {
63116
64043
  "Content-Type": "application/json",
63117
64044
  Accept: "application/json"
@@ -63121,13 +64048,15 @@ async function startDeviceAuthorization(options) {
63121
64048
  const url = `${webBase}/api/cli/auth/device`;
63122
64049
  let res;
63123
64050
  try {
63124
- res = await fetchImpl(url, {
64051
+ res = httpRetryEnabled() ? await startWithRetry(url, fetchImpl) : await fetchImpl(url, {
63125
64052
  method: "POST",
63126
64053
  headers: JSON_HEADERS,
63127
64054
  body: "{}",
63128
64055
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
63129
64056
  });
63130
64057
  } catch (err) {
64058
+ if (httpRetryEnabled())
64059
+ throw err;
63131
64060
  throw new Error(`Could not reach ${url}: ${err.message}`);
63132
64061
  }
63133
64062
  if (res.status === 404)
@@ -63137,6 +64066,8 @@ async function startDeviceAuthorization(options) {
63137
64066
  }
63138
64067
  const body = await readJson2(res);
63139
64068
  if (!body?.device_code || !body.user_code) {
64069
+ if (httpRetryEnabled())
64070
+ throw interruptedLogin();
63140
64071
  throw unsupportedDeployment(webBase);
63141
64072
  }
63142
64073
  return {
@@ -63148,12 +64079,14 @@ async function startDeviceAuthorization(options) {
63148
64079
  };
63149
64080
  }
63150
64081
  async function awaitDeviceSession(options) {
64082
+ if (httpRetryEnabled())
64083
+ return await pollWithBackoff(options);
63151
64084
  const {
63152
64085
  webBase,
63153
64086
  authorization,
63154
64087
  timeoutMs = Math.min(authorization.expiresInMs, DEFAULT_LOGIN_TIMEOUT_MS),
63155
64088
  now: now2 = Date.now,
63156
- wait = sleep,
64089
+ wait = sleep2,
63157
64090
  fetchImpl = fetch
63158
64091
  } = options;
63159
64092
  const url = `${webBase}/api/cli/auth/device/token`;
@@ -63221,6 +64154,108 @@ async function awaitDeviceSession(options) {
63221
64154
  }
63222
64155
  throw new LoginTimeoutError(timeoutMs, lastError);
63223
64156
  }
64157
+ async function pollWithBackoff(options) {
64158
+ const {
64159
+ webBase,
64160
+ authorization,
64161
+ now: now2 = Date.now,
64162
+ wait = sleep2,
64163
+ fetchImpl = fetch
64164
+ } = options;
64165
+ const timeoutMs = Math.min(options.timeoutMs ?? DEFAULT_LOGIN_TIMEOUT_MS, authorization.expiresInMs);
64166
+ const started = now2();
64167
+ const budget = createRetryBudget(timeoutMs, started);
64168
+ const url = `${webBase}/api/cli/auth/device/token`;
64169
+ let intervalMs = authorization.intervalMs;
64170
+ let failures = 0;
64171
+ let attempt2 = 0;
64172
+ let lastError;
64173
+ async function backoff(status, retryAfter, slowDown = false) {
64174
+ if (slowDown)
64175
+ intervalMs = Math.min(intervalMs + 5000, MAX_POLL_INTERVAL_MS);
64176
+ const delay2 = Math.max(intervalMs, parseRetryAfterMs(retryAfter, now2()) ?? 0) + fullJitterDelayMs(Math.max(0, failures - 1));
64177
+ const exhausted = !budget.fits(delay2, now2());
64178
+ const event = {
64179
+ event: exhausted ? "retry_exhausted" : "retry_attempt",
64180
+ layer: "http",
64181
+ op: "device_authorization_poll",
64182
+ attempt: attempt2,
64183
+ class: status === null ? "unsent" : "retryable_status",
64184
+ status,
64185
+ elapsed_ms: now2() - started,
64186
+ budget_ms: timeoutMs,
64187
+ error: status === null ? "NetworkError" : "HttpError",
64188
+ ...exhausted ? {} : { delay_ms: delay2 }
64189
+ };
64190
+ if (exhausted) {
64191
+ console.error(JSON.stringify(event));
64192
+ throw new LoginTimeoutError(timeoutMs, lastError);
64193
+ }
64194
+ console.warn(JSON.stringify(event));
64195
+ await wait(delay2);
64196
+ }
64197
+ while (budget.remainingMs(now2()) > 0) {
64198
+ attempt2++;
64199
+ let res;
64200
+ try {
64201
+ res = await fetchImpl(url, {
64202
+ method: "POST",
64203
+ headers: JSON_HEADERS,
64204
+ body: JSON.stringify({ device_code: authorization.deviceCode }),
64205
+ signal: AbortSignal.timeout(Math.max(1, Math.floor(Math.min(REQUEST_TIMEOUT_MS, budget.remainingMs(now2())))))
64206
+ });
64207
+ } catch (cause) {
64208
+ if (!isUnsentAuthRequest(cause))
64209
+ throw interruptedLogin();
64210
+ lastError = cause.message;
64211
+ if (++failures >= MAX_CONSECUTIVE_FAILURES)
64212
+ throw new Error(`Could not reach ${url}: ${lastError}`, { cause });
64213
+ await backoff(null);
64214
+ continue;
64215
+ }
64216
+ if (res.status === 404)
64217
+ throw unsupportedDeployment(webBase);
64218
+ if (res.status === 410)
64219
+ throw new Error("This login expired. Run `brainbase login` again.");
64220
+ if (res.status === 409)
64221
+ throw new Error("This login was already completed on another device. Run `brainbase login` again.");
64222
+ if (res.status === 429) {
64223
+ failures = 0;
64224
+ lastError = "the server asked us to slow down";
64225
+ await res.body?.cancel().catch(() => {});
64226
+ await backoff(res.status, res.headers.get("Retry-After"), true);
64227
+ continue;
64228
+ }
64229
+ if (!res.ok && res.status >= 500) {
64230
+ lastError = `poll returned ${res.status}`;
64231
+ await res.body?.cancel().catch(() => {});
64232
+ if (++failures >= MAX_CONSECUTIVE_FAILURES)
64233
+ throw new Error(`Could not reach ${url}: ${lastError}`);
64234
+ await backoff(res.status, res.headers.get("Retry-After"));
64235
+ continue;
64236
+ }
64237
+ const body = await readJson2(res);
64238
+ if (body?.status === "slow_down" || body?.error === "slow_down") {
64239
+ failures = 0;
64240
+ lastError = "the server asked us to slow down";
64241
+ await backoff(res.status, res.headers.get("Retry-After"), true);
64242
+ continue;
64243
+ }
64244
+ if (!res.ok) {
64245
+ throw new Error(typeof body?.error === "string" ? body.error : `The server rejected this login (${res.status}). Run \`brainbase login\` again.`);
64246
+ }
64247
+ if (!body)
64248
+ throw interruptedLogin();
64249
+ failures = 0;
64250
+ if (body.status === "ready") {
64251
+ if (!body.session)
64252
+ throw new Error("The server said the login was ready but sent no session. Run `brainbase login` again.");
64253
+ return body.session;
64254
+ }
64255
+ await wait(Math.min(intervalMs, budget.remainingMs(now2())));
64256
+ }
64257
+ throw new LoginTimeoutError(timeoutMs, lastError);
64258
+ }
63224
64259
 
63225
64260
  // src/core/open-browser.ts
63226
64261
  import { spawn } from "node:child_process";
@@ -67522,6 +68557,7 @@ function handleApiError2(err) {
67522
68557
  }
67523
68558
 
67524
68559
  // src/cli/agent-push.ts
68560
+ import { isDeepStrictEqual as isDeepStrictEqual3 } from "node:util";
67525
68561
  var import_picocolors30 = __toESM(require_picocolors(), 1);
67526
68562
 
67527
68563
  // src/core/collection-reconcile.ts
@@ -68072,8 +69108,8 @@ class LostResponseError extends ApiError {
68072
69108
  idempotencyKey;
68073
69109
  action;
68074
69110
  retryCommand;
68075
- constructor(idempotencyKey, action, retryCommand) {
68076
- super(`The connection dropped before Brainbase answered, so ${action} may or may not have happened. ` + (retryCommand ? `Run \`${retryCommand}\` to finish it without doing it twice.` : `Re-run the same command with --idempotency-key ${idempotencyKey} to finish it without doing it twice.`));
69111
+ constructor(idempotencyKey, action, retryCommand, cause) {
69112
+ super(`The connection dropped before Brainbase answered, so ${action} may or may not have happened. ` + (retryCommand ? `Run \`${retryCommand}\` to finish it without doing it twice.` : `Re-run the same command with --idempotency-key ${idempotencyKey} to finish it without doing it twice.`), undefined, undefined, cause);
68077
69113
  this.idempotencyKey = idempotencyKey;
68078
69114
  this.action = action;
68079
69115
  this.retryCommand = retryCommand;
@@ -68159,7 +69195,7 @@ function fieldMessages(fields) {
68159
69195
  return fastApiValidationMessage({ detail: fields });
68160
69196
  }
68161
69197
  function toInterfaceError(error, options = {}) {
68162
- if (error instanceof InterfaceApiError || !(error instanceof ApiError))
69198
+ if (isLostResponseError(error) || error instanceof InterfaceApiError || !(error instanceof ApiError))
68163
69199
  return error;
68164
69200
  const status = error.status;
68165
69201
  const body = error.body;
@@ -68239,6 +69275,8 @@ async function keyedWrite(action, pathname, init, key2) {
68239
69275
  } catch (error) {
68240
69276
  if (!isLostResponseError(error) || init.signal?.aborted)
68241
69277
  throw error;
69278
+ if (httpRetryEnabled())
69279
+ throw new LostResponseError(key2, action, undefined, error);
68242
69280
  }
68243
69281
  try {
68244
69282
  return await guard(send());
@@ -69276,7 +70314,7 @@ async function runAgentPush(cwd2, args) {
69276
70314
  if (config.browser.localChanged) {
69277
70315
  update2.browser_enabled = manifest.capabilities?.browser;
69278
70316
  }
69279
- cloudAgent = await api.updateAgent(agentId, update2);
70317
+ cloudAgent = await api.updateAgent(agentId, update2, httpRetryEnabled() ? { idempotent: true } : undefined);
69280
70318
  if (update2.runtime_mode !== undefined && cloudAgent.runtime_mode !== update2.runtime_mode) {
69281
70319
  throw new Error("This control plane did not apply runtime_mode. Upgrade it before retrying; the sync baseline is unchanged.");
69282
70320
  }
@@ -69311,16 +70349,27 @@ async function runAgentPush(cwd2, args) {
69311
70349
  const pushSpinner = spinner();
69312
70350
  pushSpinner.start("Pushing…");
69313
70351
  let updatedCloud;
70352
+ const pushStartedAt = Date.now();
69314
70353
  try {
69315
70354
  const reconcileEvals = evalDecision.kind !== "skip";
69316
70355
  const reconcilePlaybooks = playbookDecision.kind !== "skip";
69317
70356
  const components = outgoing.filter((c2) => (reconcileEvals || c2.type !== "eval") && (reconcilePlaybooks || c2.type !== "playbook"));
69318
- updatedCloud = await api.pushAgentManifest(agentId, {
69319
- components,
69320
- base_revision: cloud.revision,
69321
- reconcile_playbooks: reconcilePlaybooks,
69322
- reconcile_evals: reconcileEvals
69323
- });
70357
+ try {
70358
+ updatedCloud = await api.pushAgentManifest(agentId, {
70359
+ components,
70360
+ base_revision: cloud.revision,
70361
+ reconcile_playbooks: reconcilePlaybooks,
70362
+ reconcile_evals: reconcileEvals
70363
+ }, httpRetryEnabled() ? { keepalive: false } : undefined);
70364
+ } catch (err) {
70365
+ if (!httpRetryEnabled() || !isLostResponseError(err))
70366
+ throw err;
70367
+ pushSpinner.stop("Connection lost after the manifest was sent — reading the cloud manifest back…");
70368
+ updatedCloud = await reconcilePushedManifest(agentId, components, {
70369
+ reconcileEvals,
70370
+ reconcilePlaybooks
70371
+ }, pushStartedAt);
70372
+ }
69324
70373
  pushSpinner.stop(`Pushed. New revision ${updatedCloud.revision}.`);
69325
70374
  } catch (err) {
69326
70375
  pushSpinner.stop("Failed.");
@@ -69464,7 +70513,7 @@ async function pushSecrets(cwd2, agentId, plan) {
69464
70513
  const sp = spinner();
69465
70514
  sp.start("Pushing secrets…");
69466
70515
  try {
69467
- await api.putAgentSecrets(agentId, plan.secrets);
70516
+ await api.putAgentSecrets(agentId, plan.secrets, httpRetryEnabled() ? { idempotent: true } : undefined);
69468
70517
  sp.stop(`Secrets pushed (${secretDiffSummary(plan.diff)}).`);
69469
70518
  } catch (err) {
69470
70519
  sp.stop("Failed.");
@@ -69504,6 +70553,126 @@ function describeArchiveSet(evals, playbooks) {
69504
70553
  }
69505
70554
  return parts.join(" and ");
69506
70555
  }
70556
+ function logRetryEvent2(event) {
70557
+ const line = JSON.stringify(event);
70558
+ if (event.event !== "retry_deferred")
70559
+ console.error(line);
70560
+ else
70561
+ console.log(line);
70562
+ }
70563
+ function pushRecoverySignal(startedAt) {
70564
+ const remaining = HTTP_RETRY_DEADLINE_MS - (Date.now() - startedAt);
70565
+ if (remaining <= 0)
70566
+ throw new Error("Push confirmation deadline exceeded");
70567
+ return AbortSignal.timeout(remaining);
70568
+ }
70569
+ async function reconcilePushedManifest(agentId, sent, claimed, startedAt) {
70570
+ logRetryEvent2({
70571
+ event: "retry_deferred",
70572
+ layer: "http",
70573
+ op: "push_agent_manifest",
70574
+ attempt: 1,
70575
+ class: "ambiguous",
70576
+ status: null,
70577
+ elapsed_ms: Date.now() - startedAt,
70578
+ budget_ms: HTTP_RETRY_DEADLINE_MS,
70579
+ error: "NetworkApiError"
70580
+ });
70581
+ let reread;
70582
+ let divergence;
70583
+ try {
70584
+ const signal = pushRecoverySignal(startedAt);
70585
+ reread = await api.getAgentManifest(agentId, { signal });
70586
+ divergence = await manifestDivergence(reread.components, sent, claimed, signal);
70587
+ } catch (err) {
70588
+ throw new ApiError(`The manifest push may have succeeded, but the CLI could not read the agent back to confirm it (${err.message}). Run brainbase agent pull before pushing again.`);
70589
+ }
70590
+ if (divergence) {
70591
+ throw new ApiError(`Cannot confirm the push: the cloud manifest at revision ${reread.revision} does not match what was sent (${divergence}).`, 409);
70592
+ }
70593
+ return reread;
70594
+ }
70595
+ async function manifestDivergence(cloud, sent, claimed, signal) {
70596
+ const cloudByType = new Map;
70597
+ for (const c2 of cloud) {
70598
+ if (c2.type === "skill" && c2.meta?.source && !sent.some((s3) => s3.type === c2.type && s3.slug === c2.slug))
70599
+ continue;
70600
+ const slugs = cloudByType.get(c2.type) ?? new Set;
70601
+ slugs.add(c2.slug);
70602
+ cloudByType.set(c2.type, slugs);
70603
+ }
70604
+ const sentByType = new Map;
70605
+ for (const c2 of sent) {
70606
+ const slugs = sentByType.get(c2.type) ?? new Set;
70607
+ slugs.add(c2.slug);
70608
+ sentByType.set(c2.type, slugs);
70609
+ }
70610
+ const claimedTypes = new Set(["instruction", "skill", "mcp"]);
70611
+ if (claimed.reconcileEvals)
70612
+ claimedTypes.add("eval");
70613
+ if (claimed.reconcilePlaybooks)
70614
+ claimedTypes.add("playbook");
70615
+ const diffs = [];
70616
+ for (const type of claimedTypes) {
70617
+ const sentSlugs = sentByType.get(type) ?? new Set;
70618
+ const cloudSlugs = cloudByType.get(type) ?? new Set;
70619
+ const missing = [...sentSlugs].filter((s3) => !cloudSlugs.has(s3));
70620
+ const unexpected = [...cloudSlugs].filter((s3) => !sentSlugs.has(s3));
70621
+ if (missing.length === 0 && unexpected.length === 0)
70622
+ continue;
70623
+ const parts = [];
70624
+ if (missing.length)
70625
+ parts.push(`missing ${missing.join(", ")}`);
70626
+ if (unexpected.length)
70627
+ parts.push(`unexpected ${unexpected.join(", ")}`);
70628
+ diffs.push(`${type}: ${parts.join(", ")}`);
70629
+ }
70630
+ for (const component of sent) {
70631
+ const other = cloud.find((c2) => c2.type === component.type && c2.slug === component.slug);
70632
+ if (other && !await componentContentsMatch(component, other, signal)) {
70633
+ diffs.push(`${component.type}: ${component.slug} contents differ`);
70634
+ }
70635
+ }
70636
+ return diffs.length ? diffs.join("; ") : null;
70637
+ }
70638
+ async function componentContentsMatch(sent, cloud, signal) {
70639
+ switch (sent.type) {
70640
+ case "instruction":
70641
+ return sent.files[0]?.content.trim() === cloud.files[0]?.content.trim();
70642
+ case "playbook":
70643
+ return sent.files[0]?.content === cloud.files[0]?.content;
70644
+ case "skill":
70645
+ return sent.meta?.version === undefined || sent.meta.version === cloud.meta?.version;
70646
+ case "mcp": {
70647
+ const config = (component) => {
70648
+ const m3 = component.meta?.mcp;
70649
+ return m3 && {
70650
+ url: m3.url ?? null,
70651
+ command: m3.command ?? null,
70652
+ args: m3.args ?? [],
70653
+ env: m3.env ?? {},
70654
+ headers: m3.headers ?? {},
70655
+ is_enabled: m3.is_enabled ?? true
70656
+ };
70657
+ };
70658
+ return isDeepStrictEqual3(config(sent), config(cloud));
70659
+ }
70660
+ case "eval": {
70661
+ const expected = { ...sent.meta?.eval };
70662
+ const actual = cloud.meta?.eval;
70663
+ if (expected.judge_type === "agent" && typeof expected.judge_agent === "string" && typeof actual?.judge_agent === "string" && expected.judge_agent !== actual.judge_agent) {
70664
+ const judge = await api.getAgent(actual.judge_agent, { signal });
70665
+ const slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
70666
+ if (expected.judge_agent === judge.slug || expected.judge_agent === slug(judge.name)) {
70667
+ expected.judge_agent = actual.judge_agent;
70668
+ }
70669
+ }
70670
+ return isDeepStrictEqual3(expected, actual);
70671
+ }
70672
+ default:
70673
+ return false;
70674
+ }
70675
+ }
69507
70676
  function secretDiffSummary(diff2) {
69508
70677
  return `${diff2.localOnly.length} added, ${diff2.changed.length} updated, ${diff2.cloudOnly.length} removed`;
69509
70678
  }
@@ -82571,6 +83740,32 @@ var import_picocolors46 = __toESM(require_picocolors(), 1);
82571
83740
 
82572
83741
  // src/cli/interface-common.ts
82573
83742
  var import_picocolors45 = __toESM(require_picocolors(), 1);
83743
+
83744
+ // src/ui/error-causes.ts
83745
+ var explicitDebug = false;
83746
+ function enableDebugOutput() {
83747
+ explicitDebug = true;
83748
+ }
83749
+ function debugOutputEnabled() {
83750
+ return httpRetryEnabled() && (explicitDebug || !!process.env.BRAINBASE_DEBUG);
83751
+ }
83752
+ function formatErrorCauseChain(error2) {
83753
+ const lines = [];
83754
+ const seen = new Set;
83755
+ while (error2 !== undefined && error2 !== null && !seen.has(error2)) {
83756
+ seen.add(error2);
83757
+ const record5 = error2;
83758
+ const prefix = lines.length ? "Caused by: " : "";
83759
+ lines.push(...sanitizeLines(prefix + (record5.stack ?? `${record5.name ?? "Error"}: ${record5.message ?? String(error2)}`)).map(redactSensitiveText));
83760
+ if (record5.code !== undefined)
83761
+ lines.push(...sanitizeLines(`code: ${String(record5.code)}`).map(redactSensitiveText));
83762
+ error2 = record5.cause;
83763
+ }
83764
+ return lines.join(`
83765
+ `);
83766
+ }
83767
+
83768
+ // src/cli/interface-common.ts
82574
83769
  var JSON_FLAG = { key: "json", names: ["--json"], kind: "boolean" };
82575
83770
  var YES_FLAG = { key: "yes", names: ["--yes", "-y"], kind: "boolean" };
82576
83771
  var KEY_FLAG = {
@@ -82679,6 +83874,8 @@ async function runReportingErrors(json4, work) {
82679
83874
  if (!(typeof json4 === "function" ? json4() : json4))
82680
83875
  throw error2;
82681
83876
  printJson({ error: errorShape(error2) });
83877
+ if (debugOutputEnabled())
83878
+ console.error(formatErrorCauseChain(error2));
82682
83879
  process.exitCode = 1;
82683
83880
  }
82684
83881
  }
@@ -83194,6 +84391,584 @@ async function unpin(ctx) {
83194
84391
  }
83195
84392
  var writeCommands = { label, restore, pin, unpin };
83196
84393
 
84394
+ // src/core/idempotency.ts
84395
+ import { randomUUID as randomUUID4 } from "node:crypto";
84396
+ function runPlanIdempotencyKey(explicitKey) {
84397
+ return explicitKey ?? (httpRetryEnabled() ? randomUUID4() : undefined);
84398
+ }
84399
+ function runPlanRetryHint(key2, uncertain = true) {
84400
+ const uncertainty = uncertain ? "Plan creation may have reached Brainbase. " : "";
84401
+ if (!key2)
84402
+ return `${uncertainty}Check existing runs before creating another plan.`;
84403
+ if (redactSensitiveText(key2) !== key2 || !/^[A-Za-z0-9._:-]+$/.test(key2)) {
84404
+ return `${uncertainty}For a retry of the same request, use the original --idempotency-key value, not a new key.`;
84405
+ }
84406
+ return uncertain ? `${uncertainty}Re-run the same command with --idempotency-key=${key2} to recover it without creating another plan.` : `For a retry of the same request, reuse --idempotency-key=${key2}.`;
84407
+ }
84408
+
84409
+ // src/core/benchmark-api.ts
84410
+ var DEFAULT_BENCHMARK_REQUEST_TIMEOUT_MS = 30000;
84411
+ var BENCHMARK_UPLOAD_PROCESSING_ALLOWANCE_MS = 30000;
84412
+ var BENCHMARK_UPLOAD_MIN_BYTES_PER_SECOND = 256 * 1024;
84413
+
84414
+ class BenchmarkClientUsageError extends Error {
84415
+ code = "invalid_arguments";
84416
+ retryable = false;
84417
+ constructor(message) {
84418
+ super(message);
84419
+ this.name = "BenchmarkClientUsageError";
84420
+ }
84421
+ }
84422
+
84423
+ class BenchmarkApiError extends ApiError {
84424
+ code;
84425
+ requestId;
84426
+ retryable;
84427
+ constructor(message, status, body, code, requestId, retryable) {
84428
+ super(message, status, body);
84429
+ this.code = code;
84430
+ this.requestId = requestId;
84431
+ this.name = "BenchmarkApiError";
84432
+ this.retryable = retryable ?? (status === undefined || status === 408 || status === 425 || status === 429 || status !== undefined && status >= 500);
84433
+ }
84434
+ }
84435
+ function safeCode(body) {
84436
+ if (!body || typeof body !== "object")
84437
+ return;
84438
+ const object4 = body;
84439
+ const nested = object4.error && typeof object4.error === "object" ? object4.error.code : object4.detail && typeof object4.detail === "object" ? object4.detail.code : undefined;
84440
+ const code = object4.code ?? nested;
84441
+ return typeof code === "string" && /^[a-z][a-z0-9_]{0,99}$/.test(code) ? code : undefined;
84442
+ }
84443
+ function queryString(query) {
84444
+ if (!query)
84445
+ return "";
84446
+ const params = new URLSearchParams;
84447
+ for (const key2 of Object.keys(query).sort()) {
84448
+ const value3 = query[key2];
84449
+ if (value3 === undefined)
84450
+ continue;
84451
+ for (const item of Array.isArray(value3) ? value3 : [value3]) {
84452
+ params.append(key2, String(item));
84453
+ }
84454
+ }
84455
+ const encoded = params.toString();
84456
+ return encoded ? `?${encoded}` : "";
84457
+ }
84458
+ function filenameFromHeaders(headers) {
84459
+ const disposition = headers.get("Content-Disposition");
84460
+ if (!disposition)
84461
+ return null;
84462
+ const utf8 = disposition.match(/filename\*=UTF-8''([^;]+)/i)?.[1];
84463
+ if (utf8) {
84464
+ try {
84465
+ return decodeURIComponent(utf8);
84466
+ } catch {
84467
+ return utf8;
84468
+ }
84469
+ }
84470
+ return disposition.match(/filename="?([^";]+)"?/i)?.[1] ?? null;
84471
+ }
84472
+ function uploadBlob(upload) {
84473
+ if (upload.data instanceof Blob)
84474
+ return upload.data;
84475
+ return new Blob([upload.data], {
84476
+ type: upload.contentType ?? "application/octet-stream"
84477
+ });
84478
+ }
84479
+ function requestAbortContext(callerSignal, timeoutMs3) {
84480
+ const controller = new AbortController;
84481
+ let source;
84482
+ const abortFromCaller = () => {
84483
+ if (source !== undefined)
84484
+ return;
84485
+ source = "caller";
84486
+ controller.abort(callerSignal?.reason);
84487
+ };
84488
+ if (callerSignal?.aborted) {
84489
+ abortFromCaller();
84490
+ } else {
84491
+ callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
84492
+ }
84493
+ const timer = setTimeout(() => {
84494
+ if (source !== undefined)
84495
+ return;
84496
+ source = "timeout";
84497
+ controller.abort(new DOMException(`Benchmark request timed out after ${timeoutMs3} ms`, "TimeoutError"));
84498
+ }, timeoutMs3);
84499
+ timer.unref?.();
84500
+ return {
84501
+ signal: controller.signal,
84502
+ source: () => source,
84503
+ dispose: () => {
84504
+ clearTimeout(timer);
84505
+ callerSignal?.removeEventListener("abort", abortFromCaller);
84506
+ }
84507
+ };
84508
+ }
84509
+ function callerAbortError(signal) {
84510
+ return signal?.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
84511
+ }
84512
+ function idempotencyHeaders(key2) {
84513
+ const headers = new Headers;
84514
+ if (key2 === undefined)
84515
+ return headers;
84516
+ if (key2.length < 8 || key2.length > 200 || /[^\x21-\x7e]/.test(key2)) {
84517
+ throw new BenchmarkClientUsageError("Idempotency keys must be 8-200 visible ASCII characters");
84518
+ }
84519
+ headers.set("Idempotency-Key", key2);
84520
+ return headers;
84521
+ }
84522
+
84523
+ class BenchmarkApiClient {
84524
+ agentId;
84525
+ fixedBaseUrl;
84526
+ fetchImpl;
84527
+ credentialResolver;
84528
+ authRetry;
84529
+ requestTimeoutMs;
84530
+ uploadTimeoutMs;
84531
+ constructor(options) {
84532
+ this.agentId = options.agentId;
84533
+ this.fixedBaseUrl = options.baseUrl?.replace(/\/+$/, "");
84534
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
84535
+ this.credentialResolver = options.resolveCredential ?? (async () => await resolveCredential());
84536
+ this.authRetry = options.authRetry ?? sendWithAuthRetry;
84537
+ this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_BENCHMARK_REQUEST_TIMEOUT_MS;
84538
+ if (!Number.isSafeInteger(this.requestTimeoutMs) || this.requestTimeoutMs < 1) {
84539
+ throw new BenchmarkClientUsageError("Benchmark request timeout must be a positive safe integer");
84540
+ }
84541
+ this.uploadTimeoutMs = options.uploadTimeoutMs;
84542
+ if (this.uploadTimeoutMs !== undefined && (!Number.isSafeInteger(this.uploadTimeoutMs) || this.uploadTimeoutMs < 1)) {
84543
+ throw new BenchmarkClientUsageError("Benchmark upload timeout must be a positive safe integer");
84544
+ }
84545
+ }
84546
+ root(session) {
84547
+ const base2 = this.fixedBaseUrl ?? masControlPlaneBaseUrl(session);
84548
+ return `${base2}/v2/agents/${encodeURIComponent(this.agentId)}/benchmarks`;
84549
+ }
84550
+ async request(pathname, options = {}) {
84551
+ const credential = await this.credentialResolver();
84552
+ let session = credential.session;
84553
+ const headers = new Headers(options.headers);
84554
+ const idempotency = idempotencyHeaders(options.idempotencyKey);
84555
+ idempotency.forEach((value3, key2) => headers.set(key2, value3));
84556
+ if (!headers.has("Accept")) {
84557
+ headers.set("Accept", options.mode === "binary" ? "application/octet-stream, application/gzip, application/zip, application/yaml, application/json" : "application/json");
84558
+ }
84559
+ let body = options.body;
84560
+ if (body !== undefined && !(body instanceof FormData) && !(body instanceof Blob) && !(body instanceof ArrayBuffer) && !(body instanceof Uint8Array) && typeof body !== "string") {
84561
+ body = stableJsonStringify(body);
84562
+ if (!headers.has("Content-Type")) {
84563
+ headers.set("Content-Type", "application/json");
84564
+ }
84565
+ }
84566
+ const method2 = (options.method ?? "GET").toUpperCase();
84567
+ const timeoutMs3 = options.timeoutMs ?? this.requestTimeoutMs;
84568
+ const abort = requestAbortContext(options.signal, timeoutMs3);
84569
+ const send = async (refreshed) => {
84570
+ if (refreshed)
84571
+ session = refreshed;
84572
+ if (options.sendBudget && options.sendBudget.remaining <= 0) {
84573
+ throw new BenchmarkApiError("HTTP request attempt budget exhausted", undefined, null, "retry_exhausted", undefined, false);
84574
+ }
84575
+ if (options.sendBudget) {
84576
+ options.sendBudget.remaining--;
84577
+ options.sendBudget.lastStatus = undefined;
84578
+ }
84579
+ const requestHeaders = new Headers(headers);
84580
+ requestHeaders.set("Authorization", `Bearer ${session?.access_token ?? credential.bearer}`);
84581
+ const response2 = await this.fetchImpl(`${this.root(session)}${pathname}${queryString(options.query)}`, { method: method2, headers: requestHeaders, body, signal: abort.signal });
84582
+ if (options.sendBudget)
84583
+ options.sendBudget.lastStatus = response2.status;
84584
+ return response2;
84585
+ };
84586
+ let response;
84587
+ try {
84588
+ try {
84589
+ response = await this.authRetry(credential.source === "session" ? session : null, send);
84590
+ } catch (error2) {
84591
+ if (abort.source() === "caller") {
84592
+ throw callerAbortError(options.signal);
84593
+ }
84594
+ if (abort.source() === "timeout") {
84595
+ throw new BenchmarkApiError(`Benchmark request timed out after ${timeoutMs3} ms`, 408, null, "request_timeout");
84596
+ }
84597
+ const failure = new BenchmarkApiError(redactSensitiveText(`Network error: ${error2 instanceof Error ? error2.message : String(error2)}`), undefined, null, "network_error");
84598
+ if (httpRetryEnabled())
84599
+ failure.cause = error2;
84600
+ throw failure;
84601
+ }
84602
+ if (!response.ok) {
84603
+ const text4 = await response.text();
84604
+ let responseBody = text4;
84605
+ try {
84606
+ responseBody = text4 ? JSON.parse(text4) : null;
84607
+ } catch {}
84608
+ const failure = new BenchmarkApiError(redactSensitiveText(masApiErrorMessage(responseBody, response.status)), response.status, responseBody, safeCode(responseBody), response.headers.get("x-request-id") ?? response.headers.get("x-correlation-id") ?? undefined);
84609
+ if (httpRetryEnabled())
84610
+ failure.retryAfter = response.headers.get("Retry-After") ?? undefined;
84611
+ throw failure;
84612
+ }
84613
+ if (options.mode === "binary") {
84614
+ return {
84615
+ bytes: new Uint8Array(await response.arrayBuffer()),
84616
+ contentType: response.headers.get("Content-Type"),
84617
+ filename: filenameFromHeaders(response.headers)
84618
+ };
84619
+ }
84620
+ if (response.status === 204)
84621
+ return;
84622
+ const text3 = await response.text();
84623
+ if (!text3)
84624
+ return null;
84625
+ try {
84626
+ return JSON.parse(text3);
84627
+ } catch {
84628
+ throw new BenchmarkApiError("Benchmark API returned an invalid JSON response", response.status, null, "invalid_response", response.headers.get("x-request-id") ?? response.headers.get("x-correlation-id") ?? undefined, false);
84629
+ }
84630
+ } catch (error2) {
84631
+ if (abort.source() === "caller") {
84632
+ throw callerAbortError(options.signal);
84633
+ }
84634
+ if (abort.source() === "timeout") {
84635
+ throw new BenchmarkApiError(`Benchmark request timed out after ${timeoutMs3} ms`, 408, null, "request_timeout");
84636
+ }
84637
+ if (httpRetryEnabled() && !(error2 instanceof ApiError)) {
84638
+ const status = response && !response.ok ? response.status : undefined;
84639
+ const failure = new BenchmarkApiError(status === undefined ? redactSensitiveText(`Network error while reading response: ${error2 instanceof Error ? error2.message : String(error2)}`) : `Benchmark API returned HTTP ${status}, but its response body was interrupted`, status, null, status === undefined ? "network_error" : undefined, response?.headers.get("x-request-id") ?? response?.headers.get("x-correlation-id") ?? undefined);
84640
+ if (status !== undefined)
84641
+ failure.retryAfter = response?.headers.get("Retry-After") ?? undefined;
84642
+ else
84643
+ failure.cause = error2;
84644
+ throw failure;
84645
+ }
84646
+ throw error2;
84647
+ } finally {
84648
+ abort.dispose();
84649
+ }
84650
+ }
84651
+ validateManifest(manifest) {
84652
+ return this.request("/validate", {
84653
+ method: "POST",
84654
+ body: { manifest }
84655
+ });
84656
+ }
84657
+ createBenchmark(manifest, options) {
84658
+ return this.request("", {
84659
+ method: "POST",
84660
+ body: { manifest },
84661
+ ...options
84662
+ });
84663
+ }
84664
+ listBenchmarks(query = {}) {
84665
+ return this.request("", {
84666
+ query: {
84667
+ limit: query.limit,
84668
+ cursor: query.cursor,
84669
+ archived: query.archived
84670
+ }
84671
+ });
84672
+ }
84673
+ getBenchmark(benchmarkId) {
84674
+ return this.request(`/${encodeURIComponent(benchmarkId)}`);
84675
+ }
84676
+ updateBenchmark(benchmarkId, input) {
84677
+ return this.request(`/${encodeURIComponent(benchmarkId)}`, {
84678
+ method: "PATCH",
84679
+ body: input
84680
+ });
84681
+ }
84682
+ archiveBenchmark(benchmarkId, expectedVersion) {
84683
+ return this.request(`/${encodeURIComponent(benchmarkId)}/archive`, {
84684
+ method: "POST",
84685
+ body: { expected_version: expectedVersion }
84686
+ });
84687
+ }
84688
+ restoreBenchmark(benchmarkId, expectedVersion) {
84689
+ return this.request(`/${encodeURIComponent(benchmarkId)}/restore`, {
84690
+ method: "POST",
84691
+ body: { expected_version: expectedVersion }
84692
+ });
84693
+ }
84694
+ async purgeBenchmark(benchmarkId, input) {
84695
+ await this.request(`/${encodeURIComponent(benchmarkId)}/purge`, {
84696
+ method: "POST",
84697
+ body: input
84698
+ });
84699
+ }
84700
+ createRevision(benchmarkId, expectedVersion, options) {
84701
+ return this.request(`/${encodeURIComponent(benchmarkId)}/revisions`, {
84702
+ method: "POST",
84703
+ body: { expected_version: expectedVersion },
84704
+ ...options
84705
+ });
84706
+ }
84707
+ listRevisions(benchmarkId, query = {}) {
84708
+ return this.request(`/${encodeURIComponent(benchmarkId)}/revisions`, {
84709
+ query: {
84710
+ limit: query.limit,
84711
+ before_revision_number: query.beforeRevisionNumber
84712
+ }
84713
+ });
84714
+ }
84715
+ exportManifest(benchmarkId, format, revisionId) {
84716
+ return this.request(`/${encodeURIComponent(benchmarkId)}/export`, {
84717
+ mode: "binary",
84718
+ query: { format, revision_id: revisionId }
84719
+ });
84720
+ }
84721
+ exportBundle(benchmarkId, revisionId) {
84722
+ return this.request(`/${encodeURIComponent(benchmarkId)}/export`, {
84723
+ mode: "binary",
84724
+ query: { format: "bundle", revision_id: revisionId }
84725
+ });
84726
+ }
84727
+ signBenchmarkObject(benchmarkId, objectId, query = {}) {
84728
+ return this.request(`/${encodeURIComponent(benchmarkId)}/objects/${encodeURIComponent(objectId)}/signed-url`, {
84729
+ query: {
84730
+ expires_in_seconds: query.expiresInSeconds,
84731
+ download_name: query.downloadName
84732
+ }
84733
+ });
84734
+ }
84735
+ upload(pathname, upload, fields, options) {
84736
+ const form = new FormData;
84737
+ const blob = uploadBlob(upload);
84738
+ form.append("file", blob, upload.filename);
84739
+ for (const [key2, value3] of Object.entries(fields)) {
84740
+ if (value3 !== undefined)
84741
+ form.append(key2, value3);
84742
+ }
84743
+ const scaledTimeoutMs = BENCHMARK_UPLOAD_PROCESSING_ALLOWANCE_MS + Math.ceil(blob.size / BENCHMARK_UPLOAD_MIN_BYTES_PER_SECOND * 1000);
84744
+ return this.request(pathname, {
84745
+ method: "POST",
84746
+ body: form,
84747
+ timeoutMs: this.uploadTimeoutMs ?? Math.max(this.requestTimeoutMs, scaledTimeoutMs),
84748
+ ...options
84749
+ });
84750
+ }
84751
+ importManifest(upload, options) {
84752
+ return this.upload("/imports/manifest", upload, {}, options);
84753
+ }
84754
+ importCsv(upload, fields, options) {
84755
+ return this.upload("/imports/csv", upload, {
84756
+ mapping_json: stableJsonStringify(fields.mapping),
84757
+ metadata_json: stableJsonStringify(fields.metadata),
84758
+ environment_json: fields.environment === undefined ? undefined : stableJsonStringify(fields.environment),
84759
+ evaluators_json: fields.evaluators === undefined ? undefined : stableJsonStringify(fields.evaluators),
84760
+ primary_evaluator_key: fields.primaryEvaluatorKey
84761
+ }, options);
84762
+ }
84763
+ importBundle(upload, options) {
84764
+ return this.upload("/imports/bundle", upload, {}, options);
84765
+ }
84766
+ previewTaskImport(input) {
84767
+ return this.request("/imports/task/preview", {
84768
+ method: "POST",
84769
+ body: input
84770
+ });
84771
+ }
84772
+ materializeTaskImport(input) {
84773
+ return this.request("/imports/task/materialize", {
84774
+ method: "POST",
84775
+ body: input
84776
+ });
84777
+ }
84778
+ previewHuggingFaceImport(input) {
84779
+ return this.request("/imports/hugging-face/preview", {
84780
+ method: "POST",
84781
+ body: input
84782
+ });
84783
+ }
84784
+ materializeHuggingFaceImport(input, options) {
84785
+ return this.request("/imports/hugging-face/materialize", {
84786
+ method: "POST",
84787
+ body: input,
84788
+ ...options
84789
+ });
84790
+ }
84791
+ createRunPlan(benchmarkId, input, options) {
84792
+ const sendBudget = httpRetryEnabled() ? { remaining: HTTP_RETRY_MAX_ATTEMPTS } : undefined;
84793
+ const send = (signal, fixedOptions2 = options) => this.request(`/${encodeURIComponent(benchmarkId)}/runs/plan`, {
84794
+ method: "POST",
84795
+ body: input,
84796
+ ...fixedOptions2,
84797
+ signal,
84798
+ sendBudget
84799
+ });
84800
+ if (!httpRetryEnabled())
84801
+ return send();
84802
+ const fixedOptions = { ...options };
84803
+ const key2 = fixedOptions.idempotencyKey?.trim();
84804
+ fixedOptions.idempotencyKey = key2;
84805
+ let uncertain = false;
84806
+ const mayHaveApplied = (error2) => {
84807
+ const status = sendBudget?.lastStatus;
84808
+ if (status === 401)
84809
+ return false;
84810
+ if (status !== undefined && status >= 200 && status < 300)
84811
+ return true;
84812
+ const kind = classifyHttpFailure(error2, !!key2);
84813
+ return kind === "ambiguous" || kind === "retryable_status" && status !== 429 && status !== 425;
84814
+ };
84815
+ const request2 = async (signal) => {
84816
+ try {
84817
+ return await send(signal, fixedOptions);
84818
+ } catch (error2) {
84819
+ if ((sendBudget?.remaining ?? HTTP_RETRY_MAX_ATTEMPTS) < HTTP_RETRY_MAX_ATTEMPTS && mayHaveApplied(error2))
84820
+ uncertain = true;
84821
+ throw error2;
84822
+ }
84823
+ };
84824
+ return retryHttpRequest(request2, {
84825
+ idempotent: false,
84826
+ keyed: !!key2,
84827
+ budgetMs: this.requestTimeoutMs,
84828
+ attemptsUsed: () => HTTP_RETRY_MAX_ATTEMPTS - (sendBudget?.remaining ?? 0),
84829
+ retryTimeouts: false,
84830
+ op: "benchmark_run_plan"
84831
+ }).catch((error2) => {
84832
+ const original = error2 instanceof BenchmarkApiError ? error2 : undefined;
84833
+ if (original && key2)
84834
+ original.idempotencyKey = key2;
84835
+ const code = original ? safeCode(original.body) : undefined;
84836
+ const settled = original?.status === 409 && (code === "idempotency_key_reused" || code === "idempotency_resource_deleted");
84837
+ const dispatched = (sendBudget?.remaining ?? HTTP_RETRY_MAX_ATTEMPTS) < HTTP_RETRY_MAX_ATTEMPTS;
84838
+ const unknown3 = uncertain || dispatched && mayHaveApplied(error2);
84839
+ if (settled || !dispatched || !uncertain && sendBudget?.lastStatus === 401 || !unknown3 && !key2)
84840
+ throw error2;
84841
+ const failure = new BenchmarkApiError(`${runPlanRetryHint(key2, unknown3)} ${redactSensitiveText(error2 instanceof Error ? error2.message : String(error2))}`, original?.status, original?.body ?? null, original?.code ?? (original ? undefined : isHttpTimeout(error2) ? "request_timeout" : "network_error"), original?.requestId, original?.retryable);
84842
+ failure.cause = error2;
84843
+ if (key2)
84844
+ failure.idempotencyKey = key2;
84845
+ throw failure;
84846
+ });
84847
+ }
84848
+ getRunPlan(planId) {
84849
+ return this.request(`/run-plans/${encodeURIComponent(planId)}`);
84850
+ }
84851
+ confirmRunPlan(planId) {
84852
+ return this.request(`/run-plans/${encodeURIComponent(planId)}/confirm`, {
84853
+ method: "POST"
84854
+ });
84855
+ }
84856
+ confirmRun(runId) {
84857
+ return this.request(`/runs/${encodeURIComponent(runId)}/confirm`, {
84858
+ method: "POST"
84859
+ });
84860
+ }
84861
+ listRuns(query = {}) {
84862
+ return this.request("/runs", {
84863
+ query: {
84864
+ benchmark_id: query.benchmarkId,
84865
+ status: query.status,
84866
+ limit: query.limit,
84867
+ cursor: query.cursor
84868
+ }
84869
+ });
84870
+ }
84871
+ getRun(runId, options = {}) {
84872
+ return this.request(`/runs/${encodeURIComponent(runId)}`, options);
84873
+ }
84874
+ updateRunAnnotation(runId, input) {
84875
+ return this.request(`/runs/${encodeURIComponent(runId)}`, {
84876
+ method: "PATCH",
84877
+ body: input
84878
+ });
84879
+ }
84880
+ cancelRun(runId) {
84881
+ return this.request(`/runs/${encodeURIComponent(runId)}/cancel`, {
84882
+ method: "POST"
84883
+ });
84884
+ }
84885
+ resumeRun(runId) {
84886
+ return this.request(`/runs/${encodeURIComponent(runId)}/resume`, {
84887
+ method: "POST"
84888
+ });
84889
+ }
84890
+ listDiagnoses(runId, query = {}) {
84891
+ return this.request(`/runs/${encodeURIComponent(runId)}/diagnoses`, {
84892
+ query: {
84893
+ limit: query.limit,
84894
+ cursor: query.cursor
84895
+ }
84896
+ });
84897
+ }
84898
+ getRunResults(runId, baselineTargetId) {
84899
+ return this.request(`/runs/${encodeURIComponent(runId)}/results`, {
84900
+ query: { baseline_target_id: baselineTargetId }
84901
+ });
84902
+ }
84903
+ exportRunResults(runId, query = {}) {
84904
+ return this.request(`/runs/${encodeURIComponent(runId)}/results/export`, {
84905
+ mode: "binary",
84906
+ query: {
84907
+ include_billing_receipts: query.includeBillingReceipts || undefined
84908
+ }
84909
+ });
84910
+ }
84911
+ listTrials(runId, query = {}) {
84912
+ return this.request(`/runs/${encodeURIComponent(runId)}/trials`, {
84913
+ query: {
84914
+ status: query.status,
84915
+ limit: query.limit,
84916
+ cursor: query.cursor
84917
+ }
84918
+ });
84919
+ }
84920
+ listAttempts(runId, query = {}) {
84921
+ return this.request(`/runs/${encodeURIComponent(runId)}/attempts`, {
84922
+ query: {
84923
+ trial_id: query.trialId,
84924
+ status: query.status,
84925
+ limit: query.limit,
84926
+ cursor: query.cursor
84927
+ }
84928
+ });
84929
+ }
84930
+ getAttempt(runId, attemptId, query = {}) {
84931
+ return this.request(`/runs/${encodeURIComponent(runId)}/attempts/${encodeURIComponent(attemptId)}`, {
84932
+ query: {
84933
+ evaluator_limit: query.evaluatorLimit,
84934
+ evaluator_cursor: query.evaluatorCursor,
84935
+ artifact_limit: query.artifactLimit,
84936
+ artifact_cursor: query.artifactCursor
84937
+ }
84938
+ });
84939
+ }
84940
+ signAttemptArtifact(runId, attemptId, artifactId, query = {}) {
84941
+ return this.request(`/runs/${encodeURIComponent(runId)}/attempts/${encodeURIComponent(attemptId)}/artifacts/${encodeURIComponent(artifactId)}/signed-url`, {
84942
+ query: {
84943
+ expires_in_seconds: query.expiresInSeconds,
84944
+ download_name: query.downloadName
84945
+ }
84946
+ });
84947
+ }
84948
+ getSummaries(benchmarkIds, historyLimit) {
84949
+ return this.request("/analytics/summaries", {
84950
+ query: {
84951
+ benchmark_id: benchmarkIds,
84952
+ history_limit: historyLimit
84953
+ }
84954
+ });
84955
+ }
84956
+ getAnalytics(benchmarkId, historyLimit) {
84957
+ return this.request(`/${encodeURIComponent(benchmarkId)}/analytics`, {
84958
+ query: { history_limit: historyLimit }
84959
+ });
84960
+ }
84961
+ setBaseline(benchmarkId, targetKey, baselineTargetId) {
84962
+ return this.request(`/${encodeURIComponent(benchmarkId)}/baselines/${encodeURIComponent(targetKey)}`, {
84963
+ method: "PUT",
84964
+ body: { baseline_target_id: baselineTargetId }
84965
+ });
84966
+ }
84967
+ async clearBaseline(benchmarkId, targetKey) {
84968
+ await this.request(`/${encodeURIComponent(benchmarkId)}/baselines/${encodeURIComponent(targetKey)}`, { method: "DELETE" });
84969
+ }
84970
+ }
84971
+
83197
84972
  // src/cli/task-status.ts
83198
84973
  var TERMINAL_TASK_STATUSES = new Set([
83199
84974
  "success",
@@ -83208,6 +84983,65 @@ function exitCodeForTaskStatus(status) {
83208
84983
  return status === SUCCESS_STATUS ? 0 : 1;
83209
84984
  }
83210
84985
  var SIGINT_EXIT_CODE = 130;
84986
+ var EX_TEMPFAIL_EXIT_CODE = 75;
84987
+ function isRetryablePollError(error2) {
84988
+ if (!(error2 instanceof ApiError))
84989
+ return false;
84990
+ if (error2 instanceof BenchmarkApiError && error2.code === "request_timeout")
84991
+ return false;
84992
+ const status = error2.status;
84993
+ return status === undefined && (isUnsentRequestError(error2) || isLostResponseError(error2) || error2 instanceof BenchmarkApiError && error2.code === "network_error") || status === 408 || status === 425 || status === 429 || status === 500 || status === 502 || status === 503 || status === 504 || status !== undefined && status >= 520 && status <= 524;
84994
+ }
84995
+ function retryAfterHeaderOf(error2) {
84996
+ if (!(error2 instanceof ApiError))
84997
+ return null;
84998
+ const retryAfter = error2.retryAfter;
84999
+ return typeof retryAfter === "string" && retryAfter.trim() ? retryAfter : null;
85000
+ }
85001
+ function logWaitRetryEvent(record5) {
85002
+ const line = JSON.stringify(record5);
85003
+ if (record5.event === "retry_exhausted")
85004
+ console.error(line);
85005
+ else
85006
+ console.warn(line);
85007
+ }
85008
+ function pollFailureClass(error2) {
85009
+ if (isUnsentRequestError(error2))
85010
+ return "unsent";
85011
+ if (error2 instanceof ApiError && error2.status !== undefined) {
85012
+ return "retryable_status";
85013
+ }
85014
+ return "ambiguous";
85015
+ }
85016
+ function pollRetryTelemetry(event, op, error2, attempt2, startedAtMs, budgetMs, delayMs, nowMs = Date.now()) {
85017
+ return {
85018
+ event,
85019
+ layer: "http",
85020
+ op,
85021
+ attempt: attempt2,
85022
+ class: pollFailureClass(error2),
85023
+ status: error2 instanceof ApiError && error2.status !== undefined ? error2.status : null,
85024
+ elapsed_ms: nowMs - startedAtMs,
85025
+ budget_ms: budgetMs,
85026
+ error: error2 instanceof Error ? error2.name : String(error2),
85027
+ ...delayMs !== undefined ? { delay_ms: delayMs } : {}
85028
+ };
85029
+ }
85030
+ function waitUnreachableNotice(what, resumeHint, budgetMs) {
85031
+ return `${what} is unreachable; the ${Math.round(budgetMs / 1000)}s downtime retry budget is exhausted. ` + `It may still be running; ${resumeHint}.`;
85032
+ }
85033
+ function exitCodeForWaitOutcome(outcome) {
85034
+ switch (outcome.kind) {
85035
+ case "terminal":
85036
+ return exitCodeForTaskStatus(outcome.task.status);
85037
+ case "interrupted":
85038
+ return SIGINT_EXIT_CODE;
85039
+ case "unreachable":
85040
+ return EX_TEMPFAIL_EXIT_CODE;
85041
+ case "timeout":
85042
+ return 1;
85043
+ }
85044
+ }
83211
85045
  async function waitForTask(taskId, options) {
83212
85046
  const stop = new AbortController;
83213
85047
  const onExternalAbort = () => stop.abort();
@@ -83217,23 +85051,92 @@ async function waitForTask(taskId, options) {
83217
85051
  const deadlineTimer = options.timeoutMs === undefined ? null : setTimeout(() => stop.abort(), options.timeoutMs);
83218
85052
  let latest = null;
83219
85053
  let lastStatus = null;
83220
- const givenUp = () => options.signal?.aborted ? { kind: "interrupted", task: latest } : { kind: "timeout", task: latest };
85054
+ const retry = httpRetryEnabled();
85055
+ const downtimeBudgetMs = options.downtimeBudgetMs ?? DEFAULT_RETRY_BUDGET_MS;
85056
+ let downtime;
85057
+ let downtimeTimer;
85058
+ let downtimeExpired = false;
85059
+ let lastPollError;
85060
+ let failedPolls = 0;
85061
+ const now2 = options.now ?? Date.now;
85062
+ const pause2 = options.sleep ?? sleep3;
85063
+ let outageStartedAt = now2();
85064
+ const unreachable = () => {
85065
+ logWaitRetryEvent(pollRetryTelemetry("retry_exhausted", "task_status_wait", lastPollError, failedPolls, outageStartedAt, downtimeBudgetMs, undefined, now2()));
85066
+ return { kind: "unreachable", task: latest, budgetMs: downtimeBudgetMs };
85067
+ };
85068
+ const givenUp = () => {
85069
+ if (options.signal?.aborted)
85070
+ return { kind: "interrupted", task: latest };
85071
+ if (downtimeExpired || downtime && downtime.remainingMs(now2()) === 0)
85072
+ return unreachable();
85073
+ return { kind: "timeout", task: latest };
85074
+ };
83221
85075
  try {
83222
85076
  while (true) {
83223
85077
  if (stop.signal.aborted)
83224
85078
  return givenUp();
83225
- const polled = await raceAbort(masApi.getTask(taskId, { signal: stop.signal }), stop.signal);
83226
- if (polled.kind === "aborted")
83227
- return givenUp();
85079
+ if (downtime && downtime.remainingMs(now2()) === 0)
85080
+ return unreachable();
85081
+ const pollStartedAt = now2();
85082
+ const pollStop = retry ? new AbortController : stop;
85083
+ const onStop = () => pollStop.abort(stop.signal.reason);
85084
+ const requestTimer = retry ? setTimeout(() => {
85085
+ pollStop.abort(new DOMException(`Task status request timed out after ${HTTP_RETRY_DEADLINE_MS} ms`, "TimeoutError"));
85086
+ }, HTTP_RETRY_DEADLINE_MS) : undefined;
85087
+ if (retry)
85088
+ stop.signal.addEventListener("abort", onStop, { once: true });
85089
+ let polled;
85090
+ try {
85091
+ polled = await raceAbort(masApi.getTask(taskId, { signal: pollStop.signal, singleAttempt: retry }), pollStop.signal);
85092
+ } finally {
85093
+ if (requestTimer !== undefined)
85094
+ clearTimeout(requestTimer);
85095
+ if (retry)
85096
+ stop.signal.removeEventListener("abort", onStop);
85097
+ }
85098
+ if (polled.kind === "aborted") {
85099
+ if (stop.signal.aborted)
85100
+ return givenUp();
85101
+ throw pollStop.signal.reason;
85102
+ }
83228
85103
  if (polled.kind === "failed") {
83229
85104
  if (stop.signal.aborted)
83230
85105
  return givenUp();
83231
- if (!options.retryRead?.(polled.error))
85106
+ if (!retry) {
85107
+ if (!options.retryRead?.(polled.error))
85108
+ throw polled.error;
85109
+ if (await pause2(options.intervalMs, stop.signal))
85110
+ return givenUp();
85111
+ continue;
85112
+ }
85113
+ const retryable = options.retryRead ? options.retryRead(polled.error) : isRetryablePollError(polled.error);
85114
+ if (!retryable)
83232
85115
  throw polled.error;
83233
- if (await sleep2(options.intervalMs, stop.signal))
85116
+ lastPollError = polled.error;
85117
+ if (!downtime) {
85118
+ outageStartedAt = pollStartedAt;
85119
+ downtime = createRetryBudget(downtimeBudgetMs, outageStartedAt);
85120
+ downtimeTimer = setTimeout(() => {
85121
+ downtimeExpired = true;
85122
+ stop.abort();
85123
+ }, downtime.remainingMs(now2()));
85124
+ }
85125
+ failedPolls += 1;
85126
+ const delayMs = retryDelayMs(failedPolls - 1, downtime, retryAfterHeaderOf(polled.error), { maxMs: Math.min(STREAM_RETRY_MAX_DELAY_MS, Math.max(0, downtime.remainingMs(now2()) - 1)), rng: options.jitter }, now2());
85127
+ if (delayMs === null) {
85128
+ return unreachable();
85129
+ }
85130
+ logWaitRetryEvent(pollRetryTelemetry("retry_attempt", "task_status_wait", polled.error, failedPolls, outageStartedAt, downtimeBudgetMs, delayMs, now2()));
85131
+ if (await pause2(delayMs, stop.signal))
83234
85132
  return givenUp();
83235
85133
  continue;
83236
85134
  }
85135
+ downtime = undefined;
85136
+ if (downtimeTimer !== undefined)
85137
+ clearTimeout(downtimeTimer);
85138
+ downtimeTimer = undefined;
85139
+ failedPolls = 0;
83237
85140
  latest = polled.value;
83238
85141
  if (latest.status !== lastStatus) {
83239
85142
  lastStatus = latest.status;
@@ -83242,13 +85145,15 @@ async function waitForTask(taskId, options) {
83242
85145
  if (isTerminalTaskStatus(latest.status)) {
83243
85146
  return { kind: "terminal", task: latest };
83244
85147
  }
83245
- const cutShort = await sleep2(options.intervalMs, stop.signal);
85148
+ const cutShort = await pause2(options.intervalMs, stop.signal);
83246
85149
  if (cutShort)
83247
85150
  return givenUp();
83248
85151
  }
83249
85152
  } finally {
83250
85153
  if (deadlineTimer !== null)
83251
85154
  clearTimeout(deadlineTimer);
85155
+ if (downtimeTimer !== undefined)
85156
+ clearTimeout(downtimeTimer);
83252
85157
  options.signal?.removeEventListener("abort", onExternalAbort);
83253
85158
  }
83254
85159
  }
@@ -83267,7 +85172,7 @@ function raceAbort(work, signal) {
83267
85172
  });
83268
85173
  });
83269
85174
  }
83270
- function sleep2(ms2, signal) {
85175
+ function sleep3(ms2, signal) {
83271
85176
  if (signal.aborted)
83272
85177
  return Promise.resolve(true);
83273
85178
  return new Promise((resolve) => {
@@ -83353,7 +85258,7 @@ async function pollCapture(agentId, limitMs, deps, say, interrupted) {
83353
85258
  say(import_picocolors49.default.dim(" Waiting for the latest edit to be saved as a version..."));
83354
85259
  announced = true;
83355
85260
  }
83356
- if (await sleep2(Math.min(deps.pollIntervalMs ?? 2000, remaining), interrupted))
85261
+ if (await sleep3(Math.min(deps.pollIntervalMs ?? 2000, remaining), interrupted))
83357
85262
  return false;
83358
85263
  }
83359
85264
  }
@@ -84044,7 +85949,10 @@ var DECISION_MODELS = [
84044
85949
  "pplx-decider-v1.2",
84045
85950
  "d1-ultra",
84046
85951
  "jev-1.14",
84047
- "classify-1"
85952
+ "classify-1",
85953
+ "microsoft-decision-1",
85954
+ "clef-omni",
85955
+ "d1"
84048
85956
  ];
84049
85957
  var DEFAULT_DECISION_MODEL = "clef-flash";
84050
85958
  var DECISION_LIMITS = {
@@ -85650,7 +87558,7 @@ async function runOrchestrationPush(cwd2, args) {
85650
87558
  const sp = spinner();
85651
87559
  sp.start("Updating orchestration graph…");
85652
87560
  try {
85653
- const updated = await api.updateOrchestration(link2.orchestration_id, {
87561
+ const updateInput = {
85654
87562
  ...metaField("name"),
85655
87563
  ...metaField("description"),
85656
87564
  ...metaField("icon"),
@@ -85662,7 +87570,17 @@ async function runOrchestrationPush(cwd2, args) {
85662
87570
  ...sendWebhooks ? { replace_webhook_triggers: true } : {},
85663
87571
  ...sendDecisions ? { decisions: decisionsPayload } : {},
85664
87572
  ...lock?.revision != null ? { base_revision: lock.revision } : {}
85665
- });
87573
+ };
87574
+ let updated;
87575
+ const pushStartedAt = Date.now();
87576
+ try {
87577
+ updated = await api.updateOrchestration(link2.orchestration_id, updateInput, httpRetryEnabled() ? { keepalive: false } : undefined);
87578
+ } catch (err) {
87579
+ if (!httpRetryEnabled() || !isLostResponseError(err))
87580
+ throw err;
87581
+ sp.stop("Connection lost after the graph was sent — reading the cloud graph back…");
87582
+ updated = await reconcileOrchestrationUpdate(link2.orchestration_id, updateInput, sendDecisions ? localDecisions : null, slugForAgent, pushStartedAt);
87583
+ }
85666
87584
  sp.stop(`Graph updated. New cloud revision ${updated.revision}.`);
85667
87585
  const settledDecisions = sendDecisions ? cloudDecisions(updated, slugForAgent) : decisionPlan?.kind === "reconcile" ? cloudDecisionList : null;
85668
87586
  writeOrchSyncState(cwd2, {
@@ -85698,6 +87616,192 @@ async function runOrchestrationPush(cwd2, args) {
85698
87616
  process.exitCode = 1;
85699
87617
  }
85700
87618
  }
87619
+ async function reconcileOrchestrationUpdate(orchestrationId, sent, sentDecisions, slugForAgent, startedAt) {
87620
+ logRetryEvent2({
87621
+ event: "retry_deferred",
87622
+ layer: "http",
87623
+ op: "update_orchestration",
87624
+ attempt: 1,
87625
+ class: "ambiguous",
87626
+ status: null,
87627
+ elapsed_ms: Date.now() - startedAt,
87628
+ budget_ms: HTTP_RETRY_DEADLINE_MS,
87629
+ error: "NetworkApiError"
87630
+ });
87631
+ let reread;
87632
+ try {
87633
+ reread = await api.getOrchestration(orchestrationId, {
87634
+ signal: pushRecoverySignal(startedAt)
87635
+ });
87636
+ } catch (err) {
87637
+ throw new ApiError(`The graph update may have succeeded, but the CLI could not read the orchestration back to confirm it (${err.message}). Run brainbase orchestration pull before pushing again.`);
87638
+ }
87639
+ const divergence = [
87640
+ ...graphDivergence(reread, sent),
87641
+ ...sentDecisions ? decisionsDivergence(sentDecisions, reread, slugForAgent) : []
87642
+ ];
87643
+ if (divergence.length) {
87644
+ throw new ApiError(`Cannot confirm the push: the cloud graph at revision ${reread.revision} does not match what was sent (${divergence.join("; ")}).`, 409);
87645
+ }
87646
+ return reread;
87647
+ }
87648
+ function graphDivergence(cloud, sent) {
87649
+ const diffs = [];
87650
+ if (sent.members) {
87651
+ const sentMembers = new Set(sent.members);
87652
+ const cloudMembers = new Set(cloud.members.map((m3) => m3.agent_id));
87653
+ diffs.push(...setDivergence("members", sentMembers, cloudMembers));
87654
+ }
87655
+ if (sent.edges) {
87656
+ const sentEdges = sent.edges.map((e2) => JSON.stringify(canonical({
87657
+ from: e2.from_agent_id,
87658
+ to: e2.to_agent_id,
87659
+ description: e2.description ?? "",
87660
+ payload_schema: e2.payload_schema ?? {},
87661
+ settings: e2.settings ?? {}
87662
+ }))).sort();
87663
+ const cloudEdges = cloud.edges.map((e2) => JSON.stringify(canonical({
87664
+ from: e2.from_agent_id,
87665
+ to: e2.to_agent_id,
87666
+ description: e2.description ?? "",
87667
+ payload_schema: e2.payload_schema ?? {},
87668
+ settings: e2.settings ?? {}
87669
+ }))).sort();
87670
+ if (JSON.stringify(sentEdges) !== JSON.stringify(cloudEdges)) {
87671
+ diffs.push(`edges: sent ${sent.edges.length}, cloud has ${cloud.edges.length} or contents differ`);
87672
+ }
87673
+ }
87674
+ if (sent.triggers) {
87675
+ const kindOf = (t) => t.trigger_type ?? t.type ?? "schedule";
87676
+ const sentSchedules = sent.triggers.filter((t) => kindOf(t) === "schedule");
87677
+ const sentWebhooks = sent.triggers.filter((t) => kindOf(t) === "webhook");
87678
+ const cloudSchedules = (cloud.triggers ?? []).filter((t) => t.trigger_type === "schedule");
87679
+ const cloudWebhooks = (cloud.triggers ?? []).filter((t) => t.trigger_type === "webhook");
87680
+ const scheduleDiffs = triggerDivergence("schedule triggers", sentSchedules.map((t) => ({
87681
+ node_id: t.node_id,
87682
+ is_active: t.is_active ?? false,
87683
+ cron: cronOf(t.config) ?? t.cron_expression,
87684
+ props: t.config?.configured_props ?? {},
87685
+ targets: (t.edges ?? []).map(triggerEdgeProjection)
87686
+ })), cloudSchedules.map((t) => ({
87687
+ node_id: t.node_id,
87688
+ is_active: t.is_active ?? false,
87689
+ cron: cronOf(t.config),
87690
+ props: t.config?.configured_props ?? {},
87691
+ targets: t.edges.map(triggerEdgeProjection)
87692
+ })));
87693
+ diffs.push(...scheduleDiffs);
87694
+ if (sent.replace_webhook_triggers) {
87695
+ diffs.push(...triggerDivergence("webhook triggers", sentWebhooks.map((t) => ({
87696
+ node_id: t.node_id,
87697
+ is_active: t.is_active ?? false,
87698
+ public: typeof t.public === "boolean" ? t.public : webhookPublicFromConfig(t.config),
87699
+ targets: (t.edges ?? []).map(triggerEdgeProjection)
87700
+ })), cloudWebhooks.map((t) => ({
87701
+ node_id: t.node_id,
87702
+ is_active: t.is_active ?? false,
87703
+ public: typeof t.public === "boolean" ? t.public : webhookPublicFromConfig(t.config),
87704
+ targets: t.edges.map(triggerEdgeProjection)
87705
+ }))));
87706
+ }
87707
+ }
87708
+ for (const field2 of [
87709
+ "name",
87710
+ "description",
87711
+ "icon",
87712
+ "icon_color",
87713
+ "credit_limit"
87714
+ ]) {
87715
+ let expected = sent[field2];
87716
+ if (field2 === "name" && typeof expected === "string")
87717
+ expected = expected.trim();
87718
+ if ((field2 === "icon" || field2 === "icon_color") && expected === "")
87719
+ expected = null;
87720
+ if (expected !== undefined && cloud[field2] !== expected) {
87721
+ diffs.push(`${field2}: cloud has "${String(cloud[field2])}", sent "${String(expected)}"`);
87722
+ }
87723
+ }
87724
+ return diffs;
87725
+ }
87726
+ function triggerDivergence(label2, sent, cloud) {
87727
+ const diffs = setDivergence(label2, new Set(sent.map((t) => canonicalUuid2(t.node_id))), new Set(cloud.map((t) => canonicalUuid2(t.node_id))));
87728
+ const cloudById = new Map(cloud.map((t) => [canonicalUuid2(t.node_id), t]));
87729
+ for (const t of sent) {
87730
+ const other = cloudById.get(canonicalUuid2(t.node_id));
87731
+ if (!other)
87732
+ continue;
87733
+ if (t.cron !== undefined && t.cron !== other.cron) {
87734
+ diffs.push(`${label2}: ${t.node_id} cron "${other.cron ?? ""}", sent "${t.cron}"`);
87735
+ }
87736
+ if (JSON.stringify(canonical(t.props)) !== JSON.stringify(canonical(other.props))) {
87737
+ diffs.push(`${label2}: ${t.node_id} configured properties differ`);
87738
+ }
87739
+ if (t.is_active !== other.is_active) {
87740
+ diffs.push(`${label2}: ${t.node_id} is_active ${other.is_active}, sent ${t.is_active}`);
87741
+ }
87742
+ if (t.public !== undefined && (t.public ?? null) !== (other.public ?? null)) {
87743
+ diffs.push(`${label2}: ${t.node_id} public ${other.public ?? null}, sent ${t.public ?? null}`);
87744
+ }
87745
+ if (JSON.stringify([...t.targets].sort()) !== JSON.stringify([...other.targets].sort())) {
87746
+ diffs.push(`${label2}: ${t.node_id} targets differ`);
87747
+ }
87748
+ }
87749
+ return diffs;
87750
+ }
87751
+ function decisionsDivergence(sent, cloud, slugForAgent) {
87752
+ const cloudDecisionsList = cloudDecisions(cloud, slugForAgent);
87753
+ if (!cloudDecisionsList) {
87754
+ return [
87755
+ `decisions: the re-read shows none, but this push sent ${sent.length}`
87756
+ ];
87757
+ }
87758
+ const diffs = setDivergence("decisions", new Set(sent.map((d3) => d3.id)), new Set(cloudDecisionsList.map((d3) => d3.id)));
87759
+ if ([...deadDecisionLinks(cloud).values()].some((count) => count > 0)) {
87760
+ diffs.push("decisions: cloud still has dangling inputs or routes");
87761
+ }
87762
+ const cloudById = new Map(cloudDecisionsList.map((d3) => [d3.id, d3]));
87763
+ for (const d3 of sent) {
87764
+ const other = cloudById.get(d3.id);
87765
+ if (!other)
87766
+ continue;
87767
+ if (decisionHash(d3) !== decisionHash(other)) {
87768
+ diffs.push(`decisions: ${d3.name} (${d3.id}) contents differ`);
87769
+ }
87770
+ }
87771
+ return diffs;
87772
+ }
87773
+ function triggerEdgeProjection(edge) {
87774
+ return JSON.stringify(canonical({
87775
+ to_agent_id: edge.to_agent_id,
87776
+ description: edge.description ?? "",
87777
+ payload_schema: edge.payload_schema ?? {}
87778
+ }));
87779
+ }
87780
+ function cronOf(config2) {
87781
+ const cron = config2?.cron_expression;
87782
+ return typeof cron === "string" ? cron : undefined;
87783
+ }
87784
+ function canonical(value3) {
87785
+ if (Array.isArray(value3))
87786
+ return value3.map(canonical);
87787
+ if (value3 && typeof value3 === "object") {
87788
+ return Object.fromEntries(Object.entries(value3).sort(([a3], [b4]) => a3 < b4 ? -1 : a3 > b4 ? 1 : 0).map(([key2, v3]) => [key2, canonical(v3)]));
87789
+ }
87790
+ return value3;
87791
+ }
87792
+ function setDivergence(label2, sent, cloud) {
87793
+ const missing = [...sent].filter((v3) => !cloud.has(v3));
87794
+ const unexpected = [...cloud].filter((v3) => !sent.has(v3));
87795
+ if (missing.length === 0 && unexpected.length === 0)
87796
+ return [];
87797
+ const parts = [];
87798
+ if (missing.length)
87799
+ parts.push(`missing ${missing.join(", ")}`);
87800
+ if (unexpected.length) {
87801
+ parts.push(`unexpected ${unexpected.join(", ")}`);
87802
+ }
87803
+ return [`${label2}: ${parts.join(", ")}`];
87804
+ }
85701
87805
  function handleApiError6(err) {
85702
87806
  if (err instanceof ApiError) {
85703
87807
  if (err.status === 401) {
@@ -88680,7 +90784,7 @@ var import_picocolors74 = __toESM(require_picocolors(), 1);
88680
90784
 
88681
90785
  // src/cli/task-create.ts
88682
90786
  var import_picocolors70 = __toESM(require_picocolors(), 1);
88683
- import { randomUUID as randomUUID4 } from "node:crypto";
90787
+ import { randomUUID as randomUUID5 } from "node:crypto";
88684
90788
 
88685
90789
  // src/cli/task-update.ts
88686
90790
  var import_picocolors69 = __toESM(require_picocolors(), 1);
@@ -88891,7 +90995,7 @@ function isPublishScopeError(error2) {
88891
90995
  return /PAT lacks required scope:\s*publish/i.test(`${message3} ${body}`);
88892
90996
  }
88893
90997
  async function runTaskCreate(cwd2, options, dependencies = {}) {
88894
- const idempotencyKey = (dependencies.randomUUID ?? randomUUID4)();
90998
+ const idempotencyKey = (dependencies.randomUUID ?? randomUUID5)();
88895
90999
  const message3 = requiredValue(options.message, "--message");
88896
91000
  const interfaceId = optionalValue(options.interfaceId, "--interface");
88897
91001
  const agentId = await resolveAgentId2(cwd2, options.agentId, interfaceId, dependencies.readManifest ?? readManifest);
@@ -88963,8 +91067,11 @@ async function waitForCreatedTask(created, options, dependencies, version2) {
88963
91067
  onStatus: options.json ? undefined : (task) => console.log(`Status: ${task.status}`)
88964
91068
  });
88965
91069
  const status = outcome.task?.status ?? created.status;
88966
- const code = outcome.kind === "terminal" ? exitCodeForTaskStatus(status) : outcome.kind === "interrupted" ? SIGINT_EXIT_CODE : 1;
91070
+ const code = exitCodeForWaitOutcome(outcome);
88967
91071
  setExitCode(code);
91072
+ if (outcome.kind === "unreachable") {
91073
+ console.error(waitUnreachableNotice(`Task ${created.id}'s status`, `check with \`brainbase task get ${created.id}\``, outcome.budgetMs));
91074
+ }
88968
91075
  if (options.json) {
88969
91076
  console.log(JSON.stringify({
88970
91077
  task_id: created.id,
@@ -88977,6 +91084,9 @@ async function waitForCreatedTask(created, options, dependencies, version2) {
88977
91084
  }));
88978
91085
  return;
88979
91086
  }
91087
+ if (outcome.kind === "unreachable") {
91088
+ return;
91089
+ }
88980
91090
  if (outcome.kind === "timeout") {
88981
91091
  console.log(import_picocolors70.default.yellow(`Timed out after ${options.timeoutSeconds}s with the task still ${status}.`));
88982
91092
  return;
@@ -89206,7 +91316,46 @@ function failureSummary(task) {
89206
91316
  var import_picocolors73 = __toESM(require_picocolors(), 1);
89207
91317
 
89208
91318
  // src/core/task-events.ts
89209
- var DEFAULT_RETRY_BUDGET_MS = 60000;
91319
+ var LEGACY_RETRY_BUDGET_MS = 60000;
91320
+ var DEFAULT_RETRY_BUDGET_MS2 = httpRetryEnabled() ? DEFAULT_RETRY_BUDGET_MS : LEGACY_RETRY_BUDGET_MS;
91321
+ var RECONNECT_FIRST_DELAY_MS = 5000;
91322
+ var RECONNECT_MAX_DELAY_MS = 30000;
91323
+
91324
+ class FollowBudgetExhaustedError extends ApiError {
91325
+ lastEventId;
91326
+ budgetMs;
91327
+ constructor(message3, lastEventId, budgetMs) {
91328
+ super(message3);
91329
+ this.lastEventId = lastEventId;
91330
+ this.budgetMs = budgetMs;
91331
+ this.name = "FollowBudgetExhaustedError";
91332
+ }
91333
+ }
91334
+ var RETRYABLE_HTTP_STATUSES = new Set([
91335
+ 408,
91336
+ 425,
91337
+ 429,
91338
+ 500,
91339
+ 502,
91340
+ 503,
91341
+ 504,
91342
+ 520,
91343
+ 521,
91344
+ 522,
91345
+ 523,
91346
+ 524
91347
+ ]);
91348
+ var EXHAUSTED = Symbol("exhausted");
91349
+ function isRetryableHttpStatus(status) {
91350
+ return status !== undefined && RETRYABLE_HTTP_STATUSES.has(status);
91351
+ }
91352
+ function logRetryEvent3(record5) {
91353
+ const line2 = JSON.stringify(record5);
91354
+ if (record5.event === "retry_exhausted")
91355
+ console.error(line2);
91356
+ else
91357
+ console.warn(line2);
91358
+ }
89210
91359
  function followTaskEvents(options) {
89211
91360
  const {
89212
91361
  url: url2,
@@ -89215,22 +91364,124 @@ function followTaskEvents(options) {
89215
91364
  onOpen,
89216
91365
  onReconnect,
89217
91366
  signal,
89218
- retryBudgetMs = DEFAULT_RETRY_BUDGET_MS
91367
+ reconnectFirstDelayMs = RECONNECT_FIRST_DELAY_MS,
91368
+ reconnectMaxDelayMs = RECONNECT_MAX_DELAY_MS,
91369
+ now: now2 = Date.now
89219
91370
  } = options;
91371
+ const retry = httpRetryEnabled();
91372
+ const retryBudgetMs = options.retryBudgetMs ?? (retry ? DEFAULT_RETRY_BUDGET_MS : LEGACY_RETRY_BUDGET_MS);
91373
+ const idleWatchdogMs = options.idleWatchdogMs ?? IDLE_WATCHDOG_MS;
89220
91374
  return new Promise((resolve, reject2) => {
89221
91375
  if (signal?.aborted) {
89222
91376
  resolve();
89223
91377
  return;
89224
91378
  }
89225
91379
  let source;
91380
+ let sourceStop;
89226
91381
  let settled = false;
89227
91382
  let closeRequested = false;
89228
91383
  let downTimer;
91384
+ let downSince;
91385
+ let watchdogTimer;
91386
+ let reopenTimer;
89229
91387
  let lastReason = "connection lost";
91388
+ let lastEventId = null;
91389
+ let lastByteAt = now2();
91390
+ let rejectedBearer;
91391
+ let attemptBearer;
91392
+ let retryAfterMs;
91393
+ let reconnectAttempt = 0;
91394
+ let unauthorizedRetries = 0;
91395
+ const startedAt = now2();
91396
+ let lastFailureClass = "ambiguous";
91397
+ let lastFailureStatus = null;
89230
91398
  const clearBudget = () => {
89231
91399
  if (downTimer !== undefined)
89232
91400
  clearTimeout(downTimer);
89233
91401
  downTimer = undefined;
91402
+ downSince = undefined;
91403
+ };
91404
+ const armBudget = (since = now2()) => {
91405
+ if (downSince !== undefined) {
91406
+ if (now2() - downSince >= retryBudgetMs) {
91407
+ emitExhausted();
91408
+ finish(exhaustedError());
91409
+ return false;
91410
+ }
91411
+ return true;
91412
+ }
91413
+ downSince = since;
91414
+ const remaining = Math.max(0, retryBudgetMs - (now2() - since));
91415
+ downTimer = setTimeout(() => {
91416
+ downTimer = undefined;
91417
+ emitExhausted();
91418
+ finish(exhaustedError());
91419
+ }, remaining);
91420
+ return true;
91421
+ };
91422
+ const emitExhausted = (status = lastFailureStatus) => {
91423
+ if (!retry)
91424
+ return;
91425
+ logRetryEvent3({
91426
+ event: "retry_exhausted",
91427
+ layer: "http",
91428
+ op: "task_events_follow",
91429
+ attempt: Math.max(1, reconnectAttempt),
91430
+ class: lastFailureClass,
91431
+ status,
91432
+ elapsed_ms: now2() - startedAt,
91433
+ budget_ms: retryBudgetMs,
91434
+ error: "FollowBudgetExhaustedError"
91435
+ });
91436
+ };
91437
+ const exhaustedError = () => {
91438
+ const message3 = `The log stream has been unreachable for ${Math.round(retryBudgetMs / 1000)}s (${lastReason}). ` + `Giving up — the task may still be running; re-run to reattach.` + (retry ? ` Last event id: ${lastEventId ?? "(none)"}.` : "");
91439
+ if (!retry)
91440
+ return new ApiError(message3);
91441
+ return new FollowBudgetExhaustedError(message3, lastEventId, retryBudgetMs);
91442
+ };
91443
+ const clearWatchdog = () => {
91444
+ if (watchdogTimer !== undefined)
91445
+ clearTimeout(watchdogTimer);
91446
+ watchdogTimer = undefined;
91447
+ };
91448
+ const armWatchdog = () => {
91449
+ if (!retry)
91450
+ return;
91451
+ clearWatchdog();
91452
+ watchdogTimer = setTimeout(() => {
91453
+ watchdogTimer = undefined;
91454
+ if (settled || closeRequested || source === undefined)
91455
+ return;
91456
+ reconnectAttempt += 1;
91457
+ lastReason = `no data for ${Math.round(idleWatchdogMs / 1000)}s (idle watchdog)`;
91458
+ onReconnect?.(lastReason);
91459
+ const delay2 = jitterDelayMs();
91460
+ emitRetry("ambiguous", null, "IdleWatchdog", delay2);
91461
+ if (!armBudget(lastByteAt))
91462
+ return;
91463
+ closeSource();
91464
+ scheduleReopen(delay2);
91465
+ }, idleWatchdogMs);
91466
+ };
91467
+ const noteByte = () => {
91468
+ lastByteAt = now2();
91469
+ armWatchdog();
91470
+ };
91471
+ const clearReopen = () => {
91472
+ if (reopenTimer !== undefined)
91473
+ clearTimeout(reopenTimer);
91474
+ reopenTimer = undefined;
91475
+ };
91476
+ const closeSource = () => {
91477
+ clearWatchdog();
91478
+ const current = source;
91479
+ source = undefined;
91480
+ sourceStop?.abort();
91481
+ sourceStop = undefined;
91482
+ current?.close();
91483
+ if (retry && current)
91484
+ queueMicrotask(() => current.close());
89234
91485
  };
89235
91486
  const finish = (error2) => {
89236
91487
  if (settled)
@@ -89238,60 +91489,198 @@ function followTaskEvents(options) {
89238
91489
  settled = true;
89239
91490
  closeRequested = true;
89240
91491
  clearBudget();
89241
- source?.close();
91492
+ clearWatchdog();
91493
+ clearReopen();
91494
+ closeSource();
89242
91495
  if (error2)
89243
91496
  reject2(error2);
89244
91497
  else
89245
91498
  resolve();
89246
91499
  };
89247
- source = new EventSource(url2, {
89248
- fetch: async (input, init) => {
89249
- let bearer;
89250
- try {
89251
- bearer = await resolveBearer();
89252
- } catch (error2) {
89253
- if (error2 instanceof StreamHostMovedError) {
89254
- finish(error2);
91500
+ const emitRetry = (failureClass, status, errorName, delayMs) => {
91501
+ if (!retry)
91502
+ return;
91503
+ lastFailureClass = failureClass;
91504
+ lastFailureStatus = status;
91505
+ logRetryEvent3({
91506
+ event: "retry_attempt",
91507
+ layer: "http",
91508
+ op: "task_events_follow",
91509
+ attempt: reconnectAttempt,
91510
+ class: failureClass,
91511
+ status,
91512
+ elapsed_ms: now2() - startedAt,
91513
+ budget_ms: retryBudgetMs,
91514
+ error: errorName,
91515
+ delay_ms: delayMs
91516
+ });
91517
+ };
91518
+ const jitterDelayMs = () => {
91519
+ const cap = Math.min(reconnectMaxDelayMs, reconnectFirstDelayMs * 2 ** Math.max(0, reconnectAttempt - 1), downSince === undefined ? retryBudgetMs : Math.max(0, retryBudgetMs - (now2() - downSince)));
91520
+ return Math.floor(Math.random() * cap);
91521
+ };
91522
+ const reconnectDelayMs = () => {
91523
+ if (retryAfterMs === undefined)
91524
+ return jitterDelayMs();
91525
+ const remaining = retryBudgetMs - (now2() - (downSince ?? now2()));
91526
+ if (retryAfterMs > remaining)
91527
+ return EXHAUSTED;
91528
+ return retryAfterMs;
91529
+ };
91530
+ const scheduleReopen = (delayMs) => {
91531
+ if (settled || closeRequested)
91532
+ return;
91533
+ if (!armBudget())
91534
+ return;
91535
+ reopenTimer = setTimeout(() => {
91536
+ reopenTimer = undefined;
91537
+ if (!settled && !closeRequested)
91538
+ openSource();
91539
+ }, Math.max(0, delayMs));
91540
+ };
91541
+ const openSource = () => {
91542
+ if (settled || closeRequested)
91543
+ return;
91544
+ const stop = retry ? new AbortController : undefined;
91545
+ sourceStop = stop;
91546
+ const es2 = new EventSource(url2, {
91547
+ fetch: async (input, init) => {
91548
+ retryAfterMs = undefined;
91549
+ let bearer;
91550
+ try {
91551
+ bearer = await resolveBearer(retry ? rejectedBearer : undefined);
91552
+ rejectedBearer = undefined;
91553
+ attemptBearer = bearer;
91554
+ } catch (error2) {
91555
+ if (error2 instanceof StreamHostMovedError) {
91556
+ finish(error2);
91557
+ throw error2;
91558
+ }
89255
91559
  throw error2;
89256
91560
  }
89257
- throw error2;
91561
+ const headers = new Headers(init?.headers);
91562
+ headers.set("Authorization", `Bearer ${bearer}`);
91563
+ if (retry && lastEventId && !headers.has("Last-Event-ID")) {
91564
+ headers.set("Last-Event-ID", lastEventId);
91565
+ }
91566
+ const response = await fetch(input, { ...init, headers, signal: stop?.signal ?? init?.signal });
91567
+ if (!retry || response.status !== 200) {
91568
+ if (response.status !== 200) {
91569
+ retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after")) ?? undefined;
91570
+ if (retry)
91571
+ await response.body?.cancel().catch(() => {});
91572
+ }
91573
+ return response;
91574
+ }
91575
+ if (!response.body)
91576
+ return response;
91577
+ const observed = response.body.pipeThrough(new TransformStream({
91578
+ transform(chunk2, controller) {
91579
+ if (source === es2)
91580
+ noteByte();
91581
+ controller.enqueue(chunk2);
91582
+ }
91583
+ }));
91584
+ return new Response(observed, {
91585
+ status: response.status,
91586
+ statusText: response.statusText,
91587
+ headers: response.headers
91588
+ });
89258
91589
  }
89259
- const headers = new Headers(init?.headers);
89260
- headers.set("Authorization", `Bearer ${bearer}`);
89261
- return await fetch(input, { ...init, headers });
89262
- }
89263
- });
89264
- if (closeRequested)
89265
- source.close();
89266
- const dispatch = source.dispatchEvent.bind(source);
89267
- source.dispatchEvent = (event) => {
89268
- if (event instanceof MessageEvent) {
89269
- onFrame({
89270
- id: event.lastEventId,
89271
- type: event.type,
89272
- data: parseFrameData(event.data)
89273
- });
89274
- }
89275
- return dispatch(event);
91590
+ });
91591
+ source = es2;
91592
+ if (retry) {
91593
+ lastByteAt = now2();
91594
+ armWatchdog();
91595
+ }
91596
+ if (closeRequested)
91597
+ es2.close();
91598
+ const dispatch = es2.dispatchEvent.bind(es2);
91599
+ es2.dispatchEvent = (event) => {
91600
+ if (source !== es2)
91601
+ return dispatch(event);
91602
+ if (event instanceof MessageEvent) {
91603
+ if (retry) {
91604
+ clearBudget();
91605
+ reconnectAttempt = 0;
91606
+ }
91607
+ if (event.lastEventId)
91608
+ lastEventId = event.lastEventId;
91609
+ onFrame({
91610
+ id: event.lastEventId,
91611
+ type: event.type,
91612
+ data: parseFrameData(event.data)
91613
+ });
91614
+ }
91615
+ return dispatch(event);
91616
+ };
91617
+ es2.addEventListener("open", () => {
91618
+ if (source !== es2)
91619
+ return;
91620
+ unauthorizedRetries = 0;
91621
+ if (retry) {
91622
+ lastByteAt = now2();
91623
+ armWatchdog();
91624
+ } else {
91625
+ clearBudget();
91626
+ }
91627
+ onOpen?.();
91628
+ });
91629
+ es2.addEventListener("error", (event) => {
91630
+ if (source !== es2)
91631
+ return;
91632
+ const { message: message3, code } = event;
91633
+ const status = typeof code === "number" ? code : undefined;
91634
+ if (es2.readyState === es2.CLOSED) {
91635
+ clearWatchdog();
91636
+ if (retry && status === 401 && unauthorizedRetries < 1) {
91637
+ unauthorizedRetries += 1;
91638
+ rejectedBearer = attemptBearer;
91639
+ lastReason = message3?.trim() || "unauthorized";
91640
+ onReconnect?.(lastReason);
91641
+ closeSource();
91642
+ scheduleReopen(0);
91643
+ return;
91644
+ }
91645
+ if (retry && isRetryableHttpStatus(status)) {
91646
+ reconnectAttempt += 1;
91647
+ lastFailureClass = "retryable_status";
91648
+ lastFailureStatus = status ?? null;
91649
+ lastReason = message3?.trim() || `HTTP ${status}`;
91650
+ onReconnect?.(lastReason);
91651
+ if (!armBudget())
91652
+ return;
91653
+ const delay2 = reconnectDelayMs();
91654
+ if (delay2 === EXHAUSTED) {
91655
+ emitExhausted(status ?? null);
91656
+ finish(exhaustedError());
91657
+ return;
91658
+ }
91659
+ emitRetry("retryable_status", status ?? null, "ErrorEvent", delay2);
91660
+ closeSource();
91661
+ scheduleReopen(delay2);
91662
+ return;
91663
+ }
91664
+ finish(streamError(message3, code));
91665
+ return;
91666
+ }
91667
+ clearWatchdog();
91668
+ lastReason = message3?.trim() || "connection lost";
91669
+ onReconnect?.(lastReason);
91670
+ lastFailureClass = "ambiguous";
91671
+ lastFailureStatus = null;
91672
+ if (!armBudget())
91673
+ return;
91674
+ if (retry) {
91675
+ reconnectAttempt += 1;
91676
+ const delay2 = jitterDelayMs();
91677
+ emitRetry("ambiguous", null, "ErrorEvent", delay2);
91678
+ closeSource();
91679
+ scheduleReopen(delay2);
91680
+ }
91681
+ });
89276
91682
  };
89277
- source.addEventListener("open", () => {
89278
- clearBudget();
89279
- onOpen?.();
89280
- });
89281
- source.addEventListener("error", (event) => {
89282
- const { message: message3, code } = event;
89283
- if (source.readyState === source.CLOSED) {
89284
- finish(streamError(message3, code));
89285
- return;
89286
- }
89287
- lastReason = message3?.trim() || "connection lost";
89288
- onReconnect?.(lastReason);
89289
- if (downTimer === undefined) {
89290
- downTimer = setTimeout(() => {
89291
- finish(new ApiError(`The log stream has been unreachable for ${Math.round(retryBudgetMs / 1000)}s (${lastReason}). Giving up — the task may still be running; re-run to reattach.`));
89292
- }, retryBudgetMs);
89293
- }
89294
- });
91683
+ openSource();
89295
91684
  signal?.addEventListener("abort", () => finish(), { once: true });
89296
91685
  });
89297
91686
  }
@@ -89465,12 +91854,55 @@ function isPermanentRefusal(error2) {
89465
91854
  // src/cli/task-logs.ts
89466
91855
  var MAX_PAGE = 1000;
89467
91856
  var PAGE_SIZE2 = 500;
91857
+ var STREAM_EXHAUSTED_EXIT_CODE = 75;
91858
+ var PAGE_RETRY_BASE_DELAY_MS = 250;
91859
+ var PAGE_RETRY_MAX_DELAY_MS = 30000;
91860
+
91861
+ class TranscriptPageBudgetExhaustedError extends ApiError {
91862
+ collected;
91863
+ budgetMs;
91864
+ constructor(message3, collected, budgetMs) {
91865
+ super(message3);
91866
+ this.collected = collected;
91867
+ this.budgetMs = budgetMs;
91868
+ this.name = "TranscriptPageBudgetExhaustedError";
91869
+ }
91870
+ }
91871
+
91872
+ class TranscriptPageTimeoutError extends ApiError {
91873
+ collected;
91874
+ constructor(collected) {
91875
+ super("The task transcript request timed out. Collected events are shown; re-run to read the rest.");
91876
+ this.collected = collected;
91877
+ this.name = "TranscriptPageTimeoutError";
91878
+ }
91879
+ }
89468
91880
  async function runTaskLogs(taskId, options = {}, dependencies = {}) {
89469
91881
  if (options.follow) {
89470
91882
  await followTranscript(taskId, options, dependencies);
89471
91883
  return;
89472
91884
  }
89473
- const events = await readTranscript(taskId, options.limit);
91885
+ let events;
91886
+ try {
91887
+ events = await readTranscript(taskId, options.limit, {
91888
+ budgetMs: dependencies.pageBudgetMs,
91889
+ requestTimeoutMs: dependencies.pageRequestTimeoutMs,
91890
+ now: dependencies.now
91891
+ });
91892
+ } catch (error2) {
91893
+ if (error2 instanceof TranscriptPageBudgetExhaustedError || error2 instanceof TranscriptPageTimeoutError) {
91894
+ const setExitCode = dependencies.setExitCode ?? ((code) => process.exitCode = code);
91895
+ events = error2.collected;
91896
+ renderTranscript(events, options);
91897
+ console.error(import_picocolors73.default.dim(error2.message));
91898
+ setExitCode(error2 instanceof TranscriptPageBudgetExhaustedError ? STREAM_EXHAUSTED_EXIT_CODE : 1);
91899
+ return;
91900
+ }
91901
+ throw error2;
91902
+ }
91903
+ renderTranscript(events, options);
91904
+ }
91905
+ function renderTranscript(events, options) {
89474
91906
  if (options.json) {
89475
91907
  console.log(JSON.stringify({ items: events }, null, 2));
89476
91908
  return;
@@ -89518,6 +91950,9 @@ async function followTranscript(taskId, options, dependencies) {
89518
91950
  resolveBearer,
89519
91951
  signal: controller.signal,
89520
91952
  retryBudgetMs: dependencies.retryBudgetMs,
91953
+ idleWatchdogMs: dependencies.idleWatchdogMs,
91954
+ reconnectFirstDelayMs: dependencies.reconnectFirstDelayMs,
91955
+ now: dependencies.now,
89521
91956
  onFrame: (frame) => {
89522
91957
  settle.noteFrame();
89523
91958
  const event = frame.data;
@@ -89546,6 +91981,13 @@ async function followTranscript(taskId, options, dependencies) {
89546
91981
  });
89547
91982
  if (interruptCode !== undefined)
89548
91983
  setExitCode(interruptCode);
91984
+ } catch (error2) {
91985
+ if (error2 instanceof FollowBudgetExhaustedError) {
91986
+ console.error(import_picocolors73.default.red(error2.message));
91987
+ setExitCode(STREAM_EXHAUSTED_EXIT_CODE);
91988
+ return;
91989
+ }
91990
+ throw error2;
89549
91991
  } finally {
89550
91992
  settle.stop();
89551
91993
  process.off("SIGINT", onSigint);
@@ -89576,15 +92018,79 @@ function isSupersededChunk(event, rolledUp) {
89576
92018
  function laneOf(event) {
89577
92019
  return `${event.subagent_id ?? ""}\x00${event.turn_id ?? ""}`;
89578
92020
  }
89579
- async function readTranscript(taskId, limit) {
92021
+ async function readTranscript(taskId, limit, options = {}) {
92022
+ const retry = httpRetryEnabled();
92023
+ const budgetMs = options.budgetMs ?? DEFAULT_RETRY_BUDGET_MS;
92024
+ const now2 = options.now ?? Date.now;
92025
+ let startedAt = now2();
89580
92026
  const collected = [];
89581
92027
  let after2;
92028
+ let attempt2 = 0;
92029
+ let deadline = startedAt + budgetMs;
89582
92030
  while (true) {
89583
92031
  const remaining = limit === undefined ? PAGE_SIZE2 : limit - collected.length;
89584
92032
  if (remaining <= 0)
89585
92033
  break;
89586
92034
  const pageSize = Math.min(MAX_PAGE, remaining === PAGE_SIZE2 ? PAGE_SIZE2 : remaining);
89587
- const page = await masApi.listTaskEvents(taskId, { limit: pageSize, after: after2 });
92035
+ let page;
92036
+ if (retry && now2() >= deadline) {
92037
+ throw pageBudgetExhausted(collected, budgetMs, now2() - startedAt, attempt2, "ambiguous");
92038
+ }
92039
+ attempt2 += 1;
92040
+ const stop = new AbortController;
92041
+ const request2 = masApi.listTaskEvents(taskId, {
92042
+ limit: pageSize,
92043
+ after: after2,
92044
+ ...retry ? { signal: stop.signal } : {}
92045
+ });
92046
+ request2.catch(() => {});
92047
+ let pageTimer;
92048
+ try {
92049
+ page = !retry ? await request2 : await Promise.race([
92050
+ request2,
92051
+ new Promise((_4, reject2) => {
92052
+ const left = deadline - now2();
92053
+ if (left <= 0) {
92054
+ const error2 = new PageDeadlineError;
92055
+ reject2(error2);
92056
+ stop.abort(error2);
92057
+ return;
92058
+ }
92059
+ pageTimer = setTimeout(() => {
92060
+ const error2 = left <= (options.requestTimeoutMs ?? HTTP_RETRY_DEADLINE_MS) ? new PageDeadlineError : new TranscriptPageTimeoutError(collected);
92061
+ reject2(error2);
92062
+ stop.abort(error2);
92063
+ }, Math.min(left, options.requestTimeoutMs ?? HTTP_RETRY_DEADLINE_MS));
92064
+ })
92065
+ ]);
92066
+ } catch (error2) {
92067
+ if (pageTimer !== undefined)
92068
+ clearTimeout(pageTimer);
92069
+ if (error2 instanceof PageDeadlineError) {
92070
+ throw pageBudgetExhausted(collected, budgetMs, now2() - startedAt, attempt2, "ambiguous");
92071
+ }
92072
+ const failureClass = classifyPageFailure(error2);
92073
+ if (!retry || failureClass === undefined)
92074
+ throw error2;
92075
+ if (now2() >= deadline) {
92076
+ throw pageBudgetExhausted(collected, budgetMs, now2() - startedAt, attempt2, failureClass, error2 instanceof ApiError ? error2.status ?? null : null);
92077
+ }
92078
+ const cap = Math.min(PAGE_RETRY_MAX_DELAY_MS, PAGE_RETRY_BASE_DELAY_MS * 2 ** (attempt2 - 1), Math.max(0, deadline - now2()));
92079
+ const delay2 = Math.floor(Math.random() * cap);
92080
+ const retryAfter = error2 instanceof ApiError ? parseRetryAfterMs(error2.retryAfter) : undefined;
92081
+ if (retryAfter != null && retryAfter > deadline - now2()) {
92082
+ throw pageBudgetExhausted(collected, budgetMs, now2() - startedAt, attempt2, failureClass, error2 instanceof ApiError ? error2.status : undefined);
92083
+ }
92084
+ const wait = retryAfter ?? delay2;
92085
+ logPageRetry(attempt2, failureClass, error2, budgetMs, now2() - startedAt, wait);
92086
+ await new Promise((resolve) => setTimeout(resolve, wait));
92087
+ continue;
92088
+ }
92089
+ if (pageTimer !== undefined)
92090
+ clearTimeout(pageTimer);
92091
+ attempt2 = 0;
92092
+ startedAt = now2();
92093
+ deadline = startedAt + budgetMs;
89588
92094
  if (page.length < pageSize) {
89589
92095
  collected.push(...page);
89590
92096
  break;
@@ -89599,6 +92105,51 @@ async function readTranscript(taskId, limit) {
89599
92105
  }
89600
92106
  return collected;
89601
92107
  }
92108
+ function classifyPageFailure(error2) {
92109
+ if (isUnsentRequestError(error2))
92110
+ return "unsent";
92111
+ if (isLostResponseError(error2))
92112
+ return "ambiguous";
92113
+ if (error2 instanceof ApiError && isRetryableHttpStatus(error2.status)) {
92114
+ return "retryable_status";
92115
+ }
92116
+ return;
92117
+ }
92118
+
92119
+ class PageDeadlineError extends Error {
92120
+ constructor() {
92121
+ super("page budget elapsed");
92122
+ this.name = "PageDeadlineError";
92123
+ }
92124
+ }
92125
+ function pageBudgetExhausted(collected, budgetMs, elapsedMs, attempt2, failureClass, status = null) {
92126
+ logRetryEvent3({
92127
+ event: "retry_exhausted",
92128
+ layer: "http",
92129
+ op: "task_logs_page",
92130
+ attempt: attempt2,
92131
+ class: failureClass,
92132
+ status,
92133
+ elapsed_ms: elapsedMs,
92134
+ budget_ms: budgetMs,
92135
+ error: "TranscriptPageBudgetExhaustedError"
92136
+ });
92137
+ return new TranscriptPageBudgetExhaustedError(`The task transcript has been unreadable for ${Math.round(budgetMs / 1000)}s. ` + `${collected.length} event${collected.length === 1 ? "" : "s"} collected before the failure ` + "are shown; the task may still be running — re-run to read the rest.", collected, budgetMs);
92138
+ }
92139
+ function logPageRetry(attempt2, failureClass, error2, budgetMs, elapsedMs, delayCapMs) {
92140
+ logRetryEvent3({
92141
+ event: "retry_attempt",
92142
+ layer: "http",
92143
+ op: "task_logs_page",
92144
+ attempt: attempt2,
92145
+ class: failureClass,
92146
+ status: error2 instanceof ApiError ? error2.status ?? null : null,
92147
+ elapsed_ms: elapsedMs,
92148
+ budget_ms: budgetMs,
92149
+ error: error2 instanceof Error ? error2.name : String(error2),
92150
+ delay_ms: delayCapMs
92151
+ });
92152
+ }
89602
92153
  function eventTimestamp(event) {
89603
92154
  return event.ended_at || event.ts;
89604
92155
  }
@@ -89687,7 +92238,7 @@ function oneLine2(value3) {
89687
92238
  }
89688
92239
 
89689
92240
  // src/cli/task-input.ts
89690
- import { randomUUID as randomUUID5 } from "node:crypto";
92241
+ import { randomUUID as randomUUID6 } from "node:crypto";
89691
92242
  function errorCode(error2) {
89692
92243
  if (!(error2 instanceof ApiError) || !error2.body || typeof error2.body !== "object")
89693
92244
  return;
@@ -89751,7 +92302,7 @@ function onInterrupt(handler) {
89751
92302
  async function runTaskSend(taskId, message3, options = {}) {
89752
92303
  if (!message3.trim())
89753
92304
  throw new Error("The follow-up message must not be blank.");
89754
- const inputId = options.inputId?.trim().toLowerCase() ?? randomUUID5();
92305
+ const inputId = options.inputId?.trim().toLowerCase() ?? randomUUID6();
89755
92306
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(inputId)) {
89756
92307
  throw new Error("--input-id must be a UUID.");
89757
92308
  }
@@ -90689,6 +93240,11 @@ async function build(ctx) {
90689
93240
  signal: stop.signal,
90690
93241
  retryRead: isTransientReadError
90691
93242
  });
93243
+ if (settled.kind === "unreachable") {
93244
+ console.error(waitUnreachableNotice(`Conversation ${taskId}'s status`, `check with \`brainbase task get ${taskId}\``, settled.budgetMs));
93245
+ finish({ version_id: null, build_id: null, status: "task_unsettled", task_status: settled.task?.status ?? null }, EX_TEMPFAIL_EXIT_CODE);
93246
+ return;
93247
+ }
90692
93248
  if (settled.kind !== "terminal") {
90693
93249
  const interrupted = settled.kind === "interrupted";
90694
93250
  const status2 = settled.task?.status ?? null;
@@ -95254,494 +97810,6 @@ var import_picocolors85 = __toESM(require_picocolors(), 1);
95254
97810
  import fs93 from "node:fs";
95255
97811
  import path104 from "node:path";
95256
97812
 
95257
- // src/core/benchmark-api.ts
95258
- var DEFAULT_BENCHMARK_REQUEST_TIMEOUT_MS = 30000;
95259
- var BENCHMARK_UPLOAD_PROCESSING_ALLOWANCE_MS = 30000;
95260
- var BENCHMARK_UPLOAD_MIN_BYTES_PER_SECOND = 256 * 1024;
95261
-
95262
- class BenchmarkClientUsageError extends Error {
95263
- code = "invalid_arguments";
95264
- retryable = false;
95265
- constructor(message3) {
95266
- super(message3);
95267
- this.name = "BenchmarkClientUsageError";
95268
- }
95269
- }
95270
-
95271
- class BenchmarkApiError extends ApiError {
95272
- code;
95273
- requestId;
95274
- retryable;
95275
- constructor(message3, status, body, code, requestId, retryable) {
95276
- super(message3, status, body);
95277
- this.code = code;
95278
- this.requestId = requestId;
95279
- this.name = "BenchmarkApiError";
95280
- this.retryable = retryable ?? (status === undefined || status === 408 || status === 425 || status === 429 || status !== undefined && status >= 500);
95281
- }
95282
- }
95283
- function safeCode(body) {
95284
- if (!body || typeof body !== "object")
95285
- return;
95286
- const object4 = body;
95287
- const nested = object4.error && typeof object4.error === "object" ? object4.error.code : object4.detail && typeof object4.detail === "object" ? object4.detail.code : undefined;
95288
- const code = object4.code ?? nested;
95289
- return typeof code === "string" && /^[a-z][a-z0-9_]{0,99}$/.test(code) ? code : undefined;
95290
- }
95291
- function queryString(query) {
95292
- if (!query)
95293
- return "";
95294
- const params = new URLSearchParams;
95295
- for (const key2 of Object.keys(query).sort()) {
95296
- const value3 = query[key2];
95297
- if (value3 === undefined)
95298
- continue;
95299
- for (const item of Array.isArray(value3) ? value3 : [value3]) {
95300
- params.append(key2, String(item));
95301
- }
95302
- }
95303
- const encoded = params.toString();
95304
- return encoded ? `?${encoded}` : "";
95305
- }
95306
- function filenameFromHeaders(headers) {
95307
- const disposition = headers.get("Content-Disposition");
95308
- if (!disposition)
95309
- return null;
95310
- const utf8 = disposition.match(/filename\*=UTF-8''([^;]+)/i)?.[1];
95311
- if (utf8) {
95312
- try {
95313
- return decodeURIComponent(utf8);
95314
- } catch {
95315
- return utf8;
95316
- }
95317
- }
95318
- return disposition.match(/filename="?([^";]+)"?/i)?.[1] ?? null;
95319
- }
95320
- function uploadBlob(upload) {
95321
- if (upload.data instanceof Blob)
95322
- return upload.data;
95323
- return new Blob([upload.data], {
95324
- type: upload.contentType ?? "application/octet-stream"
95325
- });
95326
- }
95327
- function requestAbortContext(callerSignal, timeoutMs3) {
95328
- const controller = new AbortController;
95329
- let source;
95330
- const abortFromCaller = () => {
95331
- if (source !== undefined)
95332
- return;
95333
- source = "caller";
95334
- controller.abort(callerSignal?.reason);
95335
- };
95336
- if (callerSignal?.aborted) {
95337
- abortFromCaller();
95338
- } else {
95339
- callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
95340
- }
95341
- const timer = setTimeout(() => {
95342
- if (source !== undefined)
95343
- return;
95344
- source = "timeout";
95345
- controller.abort(new DOMException(`Benchmark request timed out after ${timeoutMs3} ms`, "TimeoutError"));
95346
- }, timeoutMs3);
95347
- timer.unref?.();
95348
- return {
95349
- signal: controller.signal,
95350
- source: () => source,
95351
- dispose: () => {
95352
- clearTimeout(timer);
95353
- callerSignal?.removeEventListener("abort", abortFromCaller);
95354
- }
95355
- };
95356
- }
95357
- function callerAbortError(signal) {
95358
- return signal?.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
95359
- }
95360
- function idempotencyHeaders(key2) {
95361
- const headers = new Headers;
95362
- if (key2 === undefined)
95363
- return headers;
95364
- if (key2.length < 8 || key2.length > 200 || /[^\x21-\x7e]/.test(key2)) {
95365
- throw new BenchmarkClientUsageError("Idempotency keys must be 8-200 visible ASCII characters");
95366
- }
95367
- headers.set("Idempotency-Key", key2);
95368
- return headers;
95369
- }
95370
-
95371
- class BenchmarkApiClient {
95372
- agentId;
95373
- fixedBaseUrl;
95374
- fetchImpl;
95375
- credentialResolver;
95376
- authRetry;
95377
- requestTimeoutMs;
95378
- uploadTimeoutMs;
95379
- constructor(options) {
95380
- this.agentId = options.agentId;
95381
- this.fixedBaseUrl = options.baseUrl?.replace(/\/+$/, "");
95382
- this.fetchImpl = options.fetch ?? globalThis.fetch;
95383
- this.credentialResolver = options.resolveCredential ?? (async () => await resolveCredential());
95384
- this.authRetry = options.authRetry ?? sendWithAuthRetry;
95385
- this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_BENCHMARK_REQUEST_TIMEOUT_MS;
95386
- if (!Number.isSafeInteger(this.requestTimeoutMs) || this.requestTimeoutMs < 1) {
95387
- throw new BenchmarkClientUsageError("Benchmark request timeout must be a positive safe integer");
95388
- }
95389
- this.uploadTimeoutMs = options.uploadTimeoutMs;
95390
- if (this.uploadTimeoutMs !== undefined && (!Number.isSafeInteger(this.uploadTimeoutMs) || this.uploadTimeoutMs < 1)) {
95391
- throw new BenchmarkClientUsageError("Benchmark upload timeout must be a positive safe integer");
95392
- }
95393
- }
95394
- root(session) {
95395
- const base2 = this.fixedBaseUrl ?? masControlPlaneBaseUrl(session);
95396
- return `${base2}/v2/agents/${encodeURIComponent(this.agentId)}/benchmarks`;
95397
- }
95398
- async request(pathname, options = {}) {
95399
- const credential = await this.credentialResolver();
95400
- let session = credential.session;
95401
- const headers = new Headers(options.headers);
95402
- const idempotency = idempotencyHeaders(options.idempotencyKey);
95403
- idempotency.forEach((value3, key2) => headers.set(key2, value3));
95404
- if (!headers.has("Accept")) {
95405
- headers.set("Accept", options.mode === "binary" ? "application/octet-stream, application/gzip, application/zip, application/yaml, application/json" : "application/json");
95406
- }
95407
- let body = options.body;
95408
- if (body !== undefined && !(body instanceof FormData) && !(body instanceof Blob) && !(body instanceof ArrayBuffer) && !(body instanceof Uint8Array) && typeof body !== "string") {
95409
- body = stableJsonStringify(body);
95410
- if (!headers.has("Content-Type")) {
95411
- headers.set("Content-Type", "application/json");
95412
- }
95413
- }
95414
- const method2 = (options.method ?? "GET").toUpperCase();
95415
- const timeoutMs3 = options.timeoutMs ?? this.requestTimeoutMs;
95416
- const abort = requestAbortContext(options.signal, timeoutMs3);
95417
- const send = async (refreshed) => {
95418
- if (refreshed)
95419
- session = refreshed;
95420
- const requestHeaders = new Headers(headers);
95421
- requestHeaders.set("Authorization", `Bearer ${session?.access_token ?? credential.bearer}`);
95422
- return await this.fetchImpl(`${this.root(session)}${pathname}${queryString(options.query)}`, { method: method2, headers: requestHeaders, body, signal: abort.signal });
95423
- };
95424
- try {
95425
- let response;
95426
- try {
95427
- response = await this.authRetry(credential.source === "session" ? session : null, send);
95428
- } catch (error2) {
95429
- if (abort.source() === "caller") {
95430
- throw callerAbortError(options.signal);
95431
- }
95432
- if (abort.source() === "timeout") {
95433
- throw new BenchmarkApiError(`Benchmark request timed out after ${timeoutMs3} ms`, 408, null, "request_timeout");
95434
- }
95435
- throw new BenchmarkApiError(redactSensitiveText(`Network error: ${error2 instanceof Error ? error2.message : String(error2)}`), undefined, null, "network_error");
95436
- }
95437
- if (!response.ok) {
95438
- const text5 = await response.text();
95439
- let responseBody = text5;
95440
- try {
95441
- responseBody = text5 ? JSON.parse(text5) : null;
95442
- } catch {}
95443
- throw new BenchmarkApiError(redactSensitiveText(masApiErrorMessage(responseBody, response.status)), response.status, responseBody, safeCode(responseBody), response.headers.get("x-request-id") ?? response.headers.get("x-correlation-id") ?? undefined);
95444
- }
95445
- if (options.mode === "binary") {
95446
- return {
95447
- bytes: new Uint8Array(await response.arrayBuffer()),
95448
- contentType: response.headers.get("Content-Type"),
95449
- filename: filenameFromHeaders(response.headers)
95450
- };
95451
- }
95452
- if (response.status === 204)
95453
- return;
95454
- const text4 = await response.text();
95455
- if (!text4)
95456
- return null;
95457
- try {
95458
- return JSON.parse(text4);
95459
- } catch {
95460
- throw new BenchmarkApiError("Benchmark API returned an invalid JSON response", response.status, null, "invalid_response", response.headers.get("x-request-id") ?? response.headers.get("x-correlation-id") ?? undefined, false);
95461
- }
95462
- } catch (error2) {
95463
- if (abort.source() === "caller") {
95464
- throw callerAbortError(options.signal);
95465
- }
95466
- if (abort.source() === "timeout") {
95467
- throw new BenchmarkApiError(`Benchmark request timed out after ${timeoutMs3} ms`, 408, null, "request_timeout");
95468
- }
95469
- throw error2;
95470
- } finally {
95471
- abort.dispose();
95472
- }
95473
- }
95474
- validateManifest(manifest) {
95475
- return this.request("/validate", {
95476
- method: "POST",
95477
- body: { manifest }
95478
- });
95479
- }
95480
- createBenchmark(manifest, options) {
95481
- return this.request("", {
95482
- method: "POST",
95483
- body: { manifest },
95484
- ...options
95485
- });
95486
- }
95487
- listBenchmarks(query = {}) {
95488
- return this.request("", {
95489
- query: {
95490
- limit: query.limit,
95491
- cursor: query.cursor,
95492
- archived: query.archived
95493
- }
95494
- });
95495
- }
95496
- getBenchmark(benchmarkId) {
95497
- return this.request(`/${encodeURIComponent(benchmarkId)}`);
95498
- }
95499
- updateBenchmark(benchmarkId, input) {
95500
- return this.request(`/${encodeURIComponent(benchmarkId)}`, {
95501
- method: "PATCH",
95502
- body: input
95503
- });
95504
- }
95505
- archiveBenchmark(benchmarkId, expectedVersion) {
95506
- return this.request(`/${encodeURIComponent(benchmarkId)}/archive`, {
95507
- method: "POST",
95508
- body: { expected_version: expectedVersion }
95509
- });
95510
- }
95511
- restoreBenchmark(benchmarkId, expectedVersion) {
95512
- return this.request(`/${encodeURIComponent(benchmarkId)}/restore`, {
95513
- method: "POST",
95514
- body: { expected_version: expectedVersion }
95515
- });
95516
- }
95517
- async purgeBenchmark(benchmarkId, input) {
95518
- await this.request(`/${encodeURIComponent(benchmarkId)}/purge`, {
95519
- method: "POST",
95520
- body: input
95521
- });
95522
- }
95523
- createRevision(benchmarkId, expectedVersion, options) {
95524
- return this.request(`/${encodeURIComponent(benchmarkId)}/revisions`, {
95525
- method: "POST",
95526
- body: { expected_version: expectedVersion },
95527
- ...options
95528
- });
95529
- }
95530
- listRevisions(benchmarkId, query = {}) {
95531
- return this.request(`/${encodeURIComponent(benchmarkId)}/revisions`, {
95532
- query: {
95533
- limit: query.limit,
95534
- before_revision_number: query.beforeRevisionNumber
95535
- }
95536
- });
95537
- }
95538
- exportManifest(benchmarkId, format3, revisionId) {
95539
- return this.request(`/${encodeURIComponent(benchmarkId)}/export`, {
95540
- mode: "binary",
95541
- query: { format: format3, revision_id: revisionId }
95542
- });
95543
- }
95544
- exportBundle(benchmarkId, revisionId) {
95545
- return this.request(`/${encodeURIComponent(benchmarkId)}/export`, {
95546
- mode: "binary",
95547
- query: { format: "bundle", revision_id: revisionId }
95548
- });
95549
- }
95550
- signBenchmarkObject(benchmarkId, objectId, query = {}) {
95551
- return this.request(`/${encodeURIComponent(benchmarkId)}/objects/${encodeURIComponent(objectId)}/signed-url`, {
95552
- query: {
95553
- expires_in_seconds: query.expiresInSeconds,
95554
- download_name: query.downloadName
95555
- }
95556
- });
95557
- }
95558
- upload(pathname, upload, fields, options) {
95559
- const form = new FormData;
95560
- const blob = uploadBlob(upload);
95561
- form.append("file", blob, upload.filename);
95562
- for (const [key2, value3] of Object.entries(fields)) {
95563
- if (value3 !== undefined)
95564
- form.append(key2, value3);
95565
- }
95566
- const scaledTimeoutMs = BENCHMARK_UPLOAD_PROCESSING_ALLOWANCE_MS + Math.ceil(blob.size / BENCHMARK_UPLOAD_MIN_BYTES_PER_SECOND * 1000);
95567
- return this.request(pathname, {
95568
- method: "POST",
95569
- body: form,
95570
- timeoutMs: this.uploadTimeoutMs ?? Math.max(this.requestTimeoutMs, scaledTimeoutMs),
95571
- ...options
95572
- });
95573
- }
95574
- importManifest(upload, options) {
95575
- return this.upload("/imports/manifest", upload, {}, options);
95576
- }
95577
- importCsv(upload, fields, options) {
95578
- return this.upload("/imports/csv", upload, {
95579
- mapping_json: stableJsonStringify(fields.mapping),
95580
- metadata_json: stableJsonStringify(fields.metadata),
95581
- environment_json: fields.environment === undefined ? undefined : stableJsonStringify(fields.environment),
95582
- evaluators_json: fields.evaluators === undefined ? undefined : stableJsonStringify(fields.evaluators),
95583
- primary_evaluator_key: fields.primaryEvaluatorKey
95584
- }, options);
95585
- }
95586
- importBundle(upload, options) {
95587
- return this.upload("/imports/bundle", upload, {}, options);
95588
- }
95589
- previewTaskImport(input) {
95590
- return this.request("/imports/task/preview", {
95591
- method: "POST",
95592
- body: input
95593
- });
95594
- }
95595
- materializeTaskImport(input) {
95596
- return this.request("/imports/task/materialize", {
95597
- method: "POST",
95598
- body: input
95599
- });
95600
- }
95601
- previewHuggingFaceImport(input) {
95602
- return this.request("/imports/hugging-face/preview", {
95603
- method: "POST",
95604
- body: input
95605
- });
95606
- }
95607
- materializeHuggingFaceImport(input, options) {
95608
- return this.request("/imports/hugging-face/materialize", {
95609
- method: "POST",
95610
- body: input,
95611
- ...options
95612
- });
95613
- }
95614
- createRunPlan(benchmarkId, input, options) {
95615
- return this.request(`/${encodeURIComponent(benchmarkId)}/runs/plan`, {
95616
- method: "POST",
95617
- body: input,
95618
- ...options
95619
- });
95620
- }
95621
- getRunPlan(planId) {
95622
- return this.request(`/run-plans/${encodeURIComponent(planId)}`);
95623
- }
95624
- confirmRunPlan(planId) {
95625
- return this.request(`/run-plans/${encodeURIComponent(planId)}/confirm`, {
95626
- method: "POST"
95627
- });
95628
- }
95629
- confirmRun(runId) {
95630
- return this.request(`/runs/${encodeURIComponent(runId)}/confirm`, {
95631
- method: "POST"
95632
- });
95633
- }
95634
- listRuns(query = {}) {
95635
- return this.request("/runs", {
95636
- query: {
95637
- benchmark_id: query.benchmarkId,
95638
- status: query.status,
95639
- limit: query.limit,
95640
- cursor: query.cursor
95641
- }
95642
- });
95643
- }
95644
- getRun(runId, options = {}) {
95645
- return this.request(`/runs/${encodeURIComponent(runId)}`, options);
95646
- }
95647
- updateRunAnnotation(runId, input) {
95648
- return this.request(`/runs/${encodeURIComponent(runId)}`, {
95649
- method: "PATCH",
95650
- body: input
95651
- });
95652
- }
95653
- cancelRun(runId) {
95654
- return this.request(`/runs/${encodeURIComponent(runId)}/cancel`, {
95655
- method: "POST"
95656
- });
95657
- }
95658
- resumeRun(runId) {
95659
- return this.request(`/runs/${encodeURIComponent(runId)}/resume`, {
95660
- method: "POST"
95661
- });
95662
- }
95663
- listDiagnoses(runId, query = {}) {
95664
- return this.request(`/runs/${encodeURIComponent(runId)}/diagnoses`, {
95665
- query: {
95666
- limit: query.limit,
95667
- cursor: query.cursor
95668
- }
95669
- });
95670
- }
95671
- getRunResults(runId, baselineTargetId) {
95672
- return this.request(`/runs/${encodeURIComponent(runId)}/results`, {
95673
- query: { baseline_target_id: baselineTargetId }
95674
- });
95675
- }
95676
- exportRunResults(runId, query = {}) {
95677
- return this.request(`/runs/${encodeURIComponent(runId)}/results/export`, {
95678
- mode: "binary",
95679
- query: {
95680
- include_billing_receipts: query.includeBillingReceipts || undefined
95681
- }
95682
- });
95683
- }
95684
- listTrials(runId, query = {}) {
95685
- return this.request(`/runs/${encodeURIComponent(runId)}/trials`, {
95686
- query: {
95687
- status: query.status,
95688
- limit: query.limit,
95689
- cursor: query.cursor
95690
- }
95691
- });
95692
- }
95693
- listAttempts(runId, query = {}) {
95694
- return this.request(`/runs/${encodeURIComponent(runId)}/attempts`, {
95695
- query: {
95696
- trial_id: query.trialId,
95697
- status: query.status,
95698
- limit: query.limit,
95699
- cursor: query.cursor
95700
- }
95701
- });
95702
- }
95703
- getAttempt(runId, attemptId, query = {}) {
95704
- return this.request(`/runs/${encodeURIComponent(runId)}/attempts/${encodeURIComponent(attemptId)}`, {
95705
- query: {
95706
- evaluator_limit: query.evaluatorLimit,
95707
- evaluator_cursor: query.evaluatorCursor,
95708
- artifact_limit: query.artifactLimit,
95709
- artifact_cursor: query.artifactCursor
95710
- }
95711
- });
95712
- }
95713
- signAttemptArtifact(runId, attemptId, artifactId, query = {}) {
95714
- return this.request(`/runs/${encodeURIComponent(runId)}/attempts/${encodeURIComponent(attemptId)}/artifacts/${encodeURIComponent(artifactId)}/signed-url`, {
95715
- query: {
95716
- expires_in_seconds: query.expiresInSeconds,
95717
- download_name: query.downloadName
95718
- }
95719
- });
95720
- }
95721
- getSummaries(benchmarkIds, historyLimit) {
95722
- return this.request("/analytics/summaries", {
95723
- query: {
95724
- benchmark_id: benchmarkIds,
95725
- history_limit: historyLimit
95726
- }
95727
- });
95728
- }
95729
- getAnalytics(benchmarkId, historyLimit) {
95730
- return this.request(`/${encodeURIComponent(benchmarkId)}/analytics`, {
95731
- query: { history_limit: historyLimit }
95732
- });
95733
- }
95734
- setBaseline(benchmarkId, targetKey, baselineTargetId) {
95735
- return this.request(`/${encodeURIComponent(benchmarkId)}/baselines/${encodeURIComponent(targetKey)}`, {
95736
- method: "PUT",
95737
- body: { baseline_target_id: baselineTargetId }
95738
- });
95739
- }
95740
- async clearBaseline(benchmarkId, targetKey) {
95741
- await this.request(`/${encodeURIComponent(benchmarkId)}/baselines/${encodeURIComponent(targetKey)}`, { method: "DELETE" });
95742
- }
95743
- }
95744
-
95745
97813
  // src/core/benchmark-authoring.ts
95746
97814
  var import_yaml9 = __toESM(require_dist(), 1);
95747
97815
  import crypto8 from "node:crypto";
@@ -98075,6 +100143,9 @@ class BenchmarkControlOutput {
98075
100143
  this.result({ items: [...page.items] }, all ? undefined : page.next_cursor);
98076
100144
  }
98077
100145
  error(error2) {
100146
+ if (debugOutputEnabled())
100147
+ this.writer.stderr(`${formatErrorCauseChain(error2)}
100148
+ `);
98078
100149
  if (this.mode.json || this.mode.jsonl) {
98079
100150
  const encoded = this.mode.jsonl ? formatJsonLine : formatJson;
98080
100151
  this.writer.stderr(encoded(automationErrorEnvelope(this.command, error2)));
@@ -98708,7 +100779,7 @@ async function runPlan(cwd2, argv, deps) {
98708
100779
  const input = runPlanInput(options, requiredOption(options.parsed, "revision"));
98709
100780
  const { client, output } = context(cwd2, "benchmark run plan", options, deps);
98710
100781
  output.result(json4(await client.createRunPlan(benchmarkId, input, {
98711
- idempotencyKey: idempotencyKey(options)
100782
+ idempotencyKey: runPlanIdempotencyKey(idempotencyKey(options))
98712
100783
  })));
98713
100784
  }
98714
100785
  async function runStart(cwd2, argv, deps) {
@@ -98739,14 +100810,73 @@ async function watchRun(client, output, runId, interval, agentId, deps) {
98739
100810
  deps.addSigintListener(onInterrupt2);
98740
100811
  let previous = "";
98741
100812
  let latest = {};
100813
+ const retry = httpRetryEnabled();
100814
+ const downtimeBudgetMs = deps.watchDowntimeBudgetMs ?? DEFAULT_RETRY_BUDGET_MS;
100815
+ let downtime;
100816
+ let downtimeTimer;
100817
+ let lastPollError;
100818
+ let downtimeExpired = false;
100819
+ let failedPolls = 0;
100820
+ const now2 = deps.now ?? Date.now;
100821
+ let outageStartedAt = now2();
100822
+ const telemetry = (event, error2, delayMs) => pollRetryTelemetry(event, "benchmark_run_watch", error2, failedPolls, outageStartedAt, downtimeBudgetMs, delayMs, now2());
100823
+ const unreachable = () => {
100824
+ logWaitRetryEvent(telemetry("retry_exhausted", lastPollError));
100825
+ deps.writer.stderr(`${waitUnreachableNotice(`The benchmark run's status (run ${runId})`, `check with \`brainbase benchmark run show ${runId}${agentId ? ` --agent ${agentId}` : ""}\``, downtimeBudgetMs)}
100826
+ `);
100827
+ deps.setExitCode(EX_TEMPFAIL_EXIT_CODE);
100828
+ return { run: latest, interrupted: false, unreachable: true };
100829
+ };
98742
100830
  try {
98743
100831
  while (!interrupted) {
100832
+ if (downtime && downtime.remainingMs(now2()) === 0)
100833
+ return unreachable();
100834
+ const pollStartedAt = now2();
98744
100835
  try {
98745
- latest = await client.getRun(runId, { signal: controller.signal });
100836
+ if (retry) {
100837
+ const polled = await raceAbort(client.getRun(runId, { signal: controller.signal }), controller.signal);
100838
+ if (polled.kind === "aborted") {
100839
+ if (downtimeExpired)
100840
+ return unreachable();
100841
+ break;
100842
+ }
100843
+ if (polled.kind === "failed")
100844
+ throw polled.error;
100845
+ latest = polled.value;
100846
+ } else {
100847
+ latest = await client.getRun(runId, { signal: controller.signal });
100848
+ }
100849
+ if (downtimeExpired)
100850
+ return unreachable();
100851
+ downtime = undefined;
100852
+ if (downtimeTimer !== undefined)
100853
+ clearTimeout(downtimeTimer);
100854
+ downtimeTimer = undefined;
100855
+ failedPolls = 0;
98746
100856
  } catch (error2) {
98747
100857
  if (interrupted)
98748
100858
  break;
98749
- throw error2;
100859
+ if (downtimeExpired)
100860
+ return unreachable();
100861
+ if (!retry || !isRetryablePollError(error2))
100862
+ throw error2;
100863
+ lastPollError = error2;
100864
+ if (!downtime) {
100865
+ outageStartedAt = pollStartedAt;
100866
+ downtime = createRetryBudget(downtimeBudgetMs, outageStartedAt);
100867
+ downtimeTimer = setTimeout(() => {
100868
+ downtimeExpired = true;
100869
+ controller.abort();
100870
+ }, downtime.remainingMs(now2()));
100871
+ }
100872
+ failedPolls += 1;
100873
+ const delayMs = retryDelayMs(failedPolls - 1, downtime, retryAfterHeaderOf(error2), { maxMs: Math.min(STREAM_RETRY_MAX_DELAY_MS, Math.max(0, downtime.remainingMs(now2()) - 1)) }, now2());
100874
+ if (delayMs === null) {
100875
+ return unreachable();
100876
+ }
100877
+ logWaitRetryEvent(telemetry("retry_attempt", error2, delayMs));
100878
+ await deps.sleep(delayMs, controller.signal);
100879
+ continue;
98750
100880
  }
98751
100881
  const digest2 = output.digest(latest);
98752
100882
  if (digest2 !== previous) {
@@ -98769,6 +100899,8 @@ async function watchRun(client, output, runId, interval, agentId, deps) {
98769
100899
  await deps.sleep(interval, controller.signal);
98770
100900
  }
98771
100901
  } finally {
100902
+ if (downtimeTimer !== undefined)
100903
+ clearTimeout(downtimeTimer);
98772
100904
  deps.removeSigintListener(onInterrupt2);
98773
100905
  }
98774
100906
  output.event("watch.interrupted", { run_id: runId });
@@ -98786,7 +100918,8 @@ async function runWatch(cwd2, argv, deps) {
98786
100918
  if (options.json) {
98787
100919
  output.result({
98788
100920
  run: watched.run,
98789
- interrupted: watched.interrupted
100921
+ interrupted: watched.interrupted,
100922
+ ...watched.unreachable ? { unreachable: true } : {}
98790
100923
  });
98791
100924
  }
98792
100925
  }
@@ -98875,7 +101008,8 @@ async function runOneShot(cwd2, argv, deps) {
98875
101008
  output.result({
98876
101009
  plan,
98877
101010
  run: watched.run,
98878
- interrupted: watched.interrupted
101011
+ interrupted: watched.interrupted,
101012
+ ...watched.unreachable ? { unreachable: true } : {}
98879
101013
  });
98880
101014
  }
98881
101015
  }
@@ -99732,7 +101866,7 @@ async function replaceAgentKey(credential) {
99732
101866
  writeJsonAtomic(decideKeysFile(), cache3);
99733
101867
  return mintAgentKey(credential.base, credential.agentId);
99734
101868
  }
99735
- function retryDelayMs(attempt2, retryAfter) {
101869
+ function retryDelayMs2(attempt2, retryAfter) {
99736
101870
  const seconds = retryAfter === null ? Number.NaN : Number(retryAfter);
99737
101871
  const base2 = Number.isFinite(seconds) ? seconds * 1000 : 1000 * 2 ** attempt2;
99738
101872
  return Math.min(Math.max(base2, 0), MAX_RETRY_DELAY_MS) + Math.random() * 500;
@@ -99747,7 +101881,7 @@ function errorMessage3(status, text4) {
99747
101881
  return `HTTP ${status}`;
99748
101882
  }
99749
101883
  async function postDecide(credential, body, options = {}) {
99750
- const sleep3 = options.sleep ?? ((ms2) => new Promise((resolve) => setTimeout(resolve, ms2)));
101884
+ const sleep4 = options.sleep ?? ((ms2) => new Promise((resolve) => setTimeout(resolve, ms2)));
99751
101885
  const payload = JSON.stringify(body);
99752
101886
  let failure = new DecideHttpError("The decide request failed", null);
99753
101887
  for (let attempt2 = 0;attempt2 < ATTEMPTS; attempt2 += 1) {
@@ -99780,7 +101914,7 @@ async function postDecide(credential, body, options = {}) {
99780
101914
  failure = new DecideHttpError(`Network error: ${error2.message}`, null);
99781
101915
  }
99782
101916
  if (attempt2 + 1 < ATTEMPTS)
99783
- await sleep3(retryDelayMs(attempt2, retryAfter));
101917
+ await sleep4(retryDelayMs2(attempt2, retryAfter));
99784
101918
  }
99785
101919
  throw failure;
99786
101920
  }
@@ -100176,7 +102310,8 @@ Questions:
100176
102310
  --model <slug> jev, kev-4b, clef, clef-flash, gpt-6-luna, reflex,
100177
102311
  joey-1, d1.1, mammoth-1, elephant-1, drex-max,
100178
102312
  pplx-decider-flash-v1.2, pplx-decider-v1.2,
100179
- d1-ultra, jev-1.14 or classify-1
102313
+ d1-ultra, jev-1.14, classify-1,
102314
+ microsoft-decision-1, clef-omni or d1
100180
102315
  (default: the server's default)
100181
102316
  --min-confidence <0-1> mark answers below this confidence as uncertain
100182
102317
 
@@ -100742,6 +102877,8 @@ function help() {
100742
102877
  out.push("");
100743
102878
  out.push(` ${import_picocolors86.default.dim("BRAINBASE_HOME")} override the local config dir (default ~/.brainbase)`);
100744
102879
  out.push(` ${import_picocolors86.default.dim("BRAINBASE_DEBUG")} print full stack traces on error (any value; unset to disable)`);
102880
+ if (httpRetryEnabled())
102881
+ out.push(` ${import_picocolors86.default.dim("--debug")} print stack traces and the full error cause chain`);
100745
102882
  out.push(` ${import_picocolors86.default.dim("BRAINBASE_WEB_URL")} override the web app URL used by login, console links and integrations`);
100746
102883
  out.push(` ${import_picocolors86.default.dim("BRAINBASE_CONTROL_PLANE_URL")} override the MAS host (/v2/cli; task and interface commands use /v2/tasks and /v2/interfaces)`);
100747
102884
  out.push(` ${import_picocolors86.default.dim("BRAINBASE_API_URL")} legacy KLS host override (uses /api/cli; proxy/registry fallback)`);
@@ -100931,6 +103068,13 @@ async function requireAuth(cmd) {
100931
103068
  }
100932
103069
  async function main() {
100933
103070
  const argv = process14.argv.slice(2);
103071
+ const separator = argv.indexOf("--");
103072
+ const debugIndex = argv.findIndex((arg, index) => arg === "--debug" && !isValueOfPriorFlag2(argv, index) && (separator < 0 || index < separator));
103073
+ const debug = httpRetryEnabled() && argv[0] !== "run" && debugIndex >= 0;
103074
+ if (debug) {
103075
+ argv.splice(debugIndex, 1);
103076
+ enableDebugOutput();
103077
+ }
100934
103078
  const cmd = argv.shift();
100935
103079
  const rawCwd = process14.cwd();
100936
103080
  const cwd2 = (() => {
@@ -101220,8 +103364,9 @@ ${message3}`));
101220
103364
  if (err instanceof ApiError && err.status === 401 && !err.message.includes("brainbase login")) {
101221
103365
  console.error(` Run ${import_picocolors86.default.cyan("brainbase login")} to connect this device.`);
101222
103366
  }
101223
- if (process14.env.BRAINBASE_DEBUG)
101224
- console.error(err.stack);
103367
+ if (debug || process14.env.BRAINBASE_DEBUG) {
103368
+ console.error(debug || httpRetryEnabled() ? formatErrorCauseChain(err) : err.stack);
103369
+ }
101225
103370
  process14.exit(1);
101226
103371
  }
101227
103372
  }