@plune-ai/cli 0.24.0 → 0.26.0

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.
@@ -95,12 +95,14 @@ function parseRetryAfter(raw, now = Date.now()) {
95
95
  if (!Number.isFinite(seconds)) return null;
96
96
  return Math.min(Math.max(seconds, 0) * 1e3, RETRY_AFTER_CAP_MS);
97
97
  }
98
+ var POOLS = { "http:": new import_node_http.Agent({ keepAlive: true }), "https:": new import_node_https.Agent({ keepAlive: true }) };
98
99
  var IDLE_MS = 3e5;
99
100
  function nodeTransport(url, init, idleMs = IDLE_MS) {
100
101
  return new Promise((resolve2, reject) => {
101
102
  const target = new URL(url);
102
103
  const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
103
- const req = send(target, { method: init.method, headers: init.headers }, (res) => {
104
+ const agent = POOLS[target.protocol === "https:" ? "https:" : "http:"];
105
+ const req = send(target, { method: init.method, headers: init.headers, agent }, (res) => {
104
106
  const chunks = [];
105
107
  res.on("data", (chunk2) => chunks.push(chunk2));
106
108
  res.on("error", reject);
@@ -161,14 +163,25 @@ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
161
163
  async function detailOf(res) {
162
164
  try {
163
165
  const body = await res.json();
166
+ const q = body.quota;
164
167
  return {
168
+ ...typeof body.shardsExpected === "number" && Number.isInteger(body.shardsExpected) ? { shardsExpected: body.shardsExpected } : {},
165
169
  detail: typeof body.error === "string" ? body.error : "",
170
+ ...typeof q === "object" && q !== null && !Array.isArray(q) ? { quota: q } : {},
166
171
  ...typeof body.code === "string" && /^[a-z0-9_]{1,60}$/.test(body.code) ? { code: body.code } : {}
167
172
  };
168
173
  } catch {
169
174
  return { detail: "" };
170
175
  }
171
176
  }
177
+ function describeQuota(q) {
178
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
179
+ const kind = typeof q["kind"] === "string" ? q["kind"].slice(0, 60) : "limit";
180
+ const [used, limit] = [num(q["used"]), num(q["limit"])];
181
+ const parts = [used !== void 0 && limit !== void 0 ? `${kind}: ${used} of ${limit} used` : kind];
182
+ if (typeof q["resetsAt"] === "string" && q["resetsAt"] !== "") parts.push(`resets ${q["resetsAt"].slice(0, 40)}`);
183
+ return parts.join(", ");
184
+ }
172
185
  function classify(status) {
173
186
  if (status === 401 || status === 403) return "auth";
174
187
  if (status === 409) return "conflict";
@@ -233,12 +246,12 @@ function createClient(opts) {
233
246
  }
234
247
  const said = await detailOf(res);
235
248
  const kind = classify(res.status);
236
- last = { kind, status: res.status, detail: scrub(said.detail), ...said.code !== void 0 ? { code: said.code } : {} };
249
+ last = { kind, status: res.status, detail: scrub(said.detail), ...said.code !== void 0 ? { code: said.code } : {}, ...said.shardsExpected !== void 0 ? { shardsExpected: said.shardsExpected } : {} };
237
250
  if (kind !== "unavailable") break;
238
251
  const said429 = parseRetryAfter(res.headers.get("retry-after"), now());
239
- if (res.status === 429 && said429 === null) {
252
+ if (res.status === 429 && (said.quota !== void 0 || said429 === null)) {
240
253
  last.kind = "invalid";
241
- last.detail = "quota exceeded (no Retry-After)";
254
+ last.detail = `quota exceeded (${said.quota === void 0 ? "no Retry-After" : scrub(describeQuota(said.quota))})`;
242
255
  if (said.detail) last.detail += ` \u2014 ${scrub(said.detail)}`;
243
256
  break;
244
257
  }
@@ -251,6 +264,7 @@ function createClient(opts) {
251
264
  post: (path3, body, retries) => send("POST", path3, { body: JSON.stringify(body), type: "application/json" }, retries),
252
265
  upload: (path3, bytes, contentType) => send("POST", path3, { body: bytes, type: contentType }, uploadRetries ?? DEFAULT_RETRIES),
253
266
  del: (path3) => send("DELETE", path3),
267
+ get: (path3) => send("GET", path3),
254
268
  async put(url, headers, source) {
255
269
  const putRetries = uploadRetries ?? 3;
256
270
  let last = { ok: false, status: null, code: "network" };
@@ -289,7 +303,18 @@ function createClient(opts) {
289
303
  // src/reporter-core/env.ts
290
304
  var path = __toESM(require("path"), 1);
291
305
  var UNSUPPORTED_VARS = ["PLUNE_GROUP"];
292
- var UPLOAD_CONCURRENCY = { min: 1, max: 32, default: 4 };
306
+ var SHARDS_MAX = 1e3;
307
+ function shardOf(env) {
308
+ const raw = value(env, "PLUNE_SHARD");
309
+ if (raw === void 0) return {};
310
+ const m = /^(\d{1,4})\/(\d{1,4})$/.exec(raw);
311
+ const [k, n] = m ? [Number(m[1]), Number(m[2])] : [0, 0];
312
+ if (k >= 1 && k <= n && n <= SHARDS_MAX) return { shard: { index: k, total: n } };
313
+ return {
314
+ error: `PLUNE_SHARD=${JSON.stringify(raw.slice(0, 40))} is not "k/N" with 1 <= k <= N <= ${SHARDS_MAX} (for example 2/4) - nothing was sent.`
315
+ };
316
+ }
317
+ var UPLOAD_CONCURRENCY = { min: 1, max: 32, default: 16 };
293
318
  var UPLOAD_RETRIES = { min: 0, max: 20, default: 6 };
294
319
  function value(env, name) {
295
320
  const raw = env[name]?.trim();
@@ -333,6 +358,7 @@ function readEnv(env = process.env) {
333
358
  const labels = list(env, "PLUNE_LABELS");
334
359
  const offerDiscovered = flag(env, "PLUNE_CREATE");
335
360
  const full = flag(env, "PLUNE_FULL_RUN");
361
+ const { shard, error: shardError } = shardOf(env);
336
362
  return {
337
363
  config: {
338
364
  ...externalKey !== void 0 ? { externalKey } : {},
@@ -353,7 +379,11 @@ function readEnv(env = process.env) {
353
379
  // Two names for two situations that end the same way. `SHARED_RUN` says other processes are
354
380
  // reporting into this run; `PROCEED` says the job will close it on its own schedule. Either
355
381
  // way this process must not be the one to call it finished.
356
- keepOpen: flag(env, "PLUNE_SHARED_RUN") || flag(env, "PLUNE_PROCEED"),
382
+ // `PLUNE_SHARD` implies it: the run is joined by the shared key and this process does not close it
383
+ // by itself; `leaveOpen` reports the shard instead.
384
+ keepOpen: flag(env, "PLUNE_SHARED_RUN") || flag(env, "PLUNE_PROCEED") || shard !== void 0 || shardError !== void 0,
385
+ ...shard !== void 0 ? { shard } : {},
386
+ ...shardError !== void 0 ? { shardError } : {},
357
387
  ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0),
358
388
  warnings
359
389
  };
@@ -563,6 +593,7 @@ var RESOLVE_CHUNK = 500;
563
593
  var BATCH_MAX = 500;
564
594
  var DISCOVER_MAX = 500;
565
595
  var BATCH_DEFAULT = 500;
596
+ var SEND_CONCURRENCY = 4;
566
597
  var EXPECTED_MAX = 1e4;
567
598
  var KEY_MAX = 1024;
568
599
  var TITLE_MAX = 300;
@@ -700,6 +731,7 @@ function warnIfDropped(sent, run, log) {
700
731
  }
701
732
  async function startRun(passed, declared) {
702
733
  const env = readEnv();
734
+ if (env.shardError !== void 0) throw new Error(env.shardError);
703
735
  const stated = Object.fromEntries(Object.entries(passed).filter(([, v]) => v !== void 0));
704
736
  const cfg = { ...env.config, ...stated };
705
737
  const log = cfg.log ?? ((line) => void process.stderr.write(`${line}
@@ -741,6 +773,9 @@ async function startRun(passed, declared) {
741
773
  const resolved = /* @__PURE__ */ new Map();
742
774
  let buffer = [];
743
775
  const discoveries = /* @__PURE__ */ new Map();
776
+ const held = /* @__PURE__ */ new Map();
777
+ let createdWithResult = 0;
778
+ let createdUndelivered = 0;
744
779
  let runId = null;
745
780
  let joined = false;
746
781
  let offline = token === "";
@@ -869,6 +904,7 @@ async function startRun(passed, declared) {
869
904
  specRef: result.specRef,
870
905
  rawStatus: result.rawStatus
871
906
  });
907
+ held.set(result.keys[0].value, result);
872
908
  }
873
909
  async function offer() {
874
910
  if (discoveries.size === 0 || offline || runId === null) return;
@@ -887,7 +923,24 @@ async function startRun(passed, declared) {
887
923
  return;
888
924
  }
889
925
  stats.offered += out.body.results.filter((r) => r?.outcome === "queued").length;
890
- stats.created += out.body.results.filter((r) => r?.outcome === "created").length;
926
+ const made = out.body.results.filter((r) => r?.outcome === "created");
927
+ stats.created += made.length;
928
+ const submissions = [];
929
+ const filesByKey = /* @__PURE__ */ new Map();
930
+ for (const r of made) {
931
+ const pending = typeof r.id === "string" && r.id !== "" ? held.get(r.key?.value) : void 0;
932
+ if (pending === void 0) continue;
933
+ held.delete(r.key.value);
934
+ const { keys: _keys, files, ...rest } = pending;
935
+ submissions.push({ ...rest, testCaseId: r.id });
936
+ if (files !== void 0) filesByKey.set(pending.resultKey, files);
937
+ }
938
+ if (submissions.length > 0) {
939
+ const before = stats.deferred;
940
+ await send(submissions, filesByKey);
941
+ createdWithResult += submissions.length;
942
+ createdUndelivered += stats.deferred - before;
943
+ }
891
944
  }
892
945
  }
893
946
  function skip(kind, name, why) {
@@ -1294,7 +1347,29 @@ async function startRun(passed, declared) {
1294
1347
  else await uploadBatched(jobs);
1295
1348
  uploadMs += Date.now() - started;
1296
1349
  }
1350
+ const sending = /* @__PURE__ */ new Set();
1351
+ let sendError;
1352
+ let answered = false;
1353
+ async function dispatch(work) {
1354
+ while (sending.size >= (answered ? SEND_CONCURRENCY : 1)) await Promise.race(sending);
1355
+ const task = work().catch((err) => {
1356
+ sendError ??= err;
1357
+ }).finally(() => sending.delete(task));
1358
+ sending.add(task);
1359
+ }
1360
+ async function settle() {
1361
+ while (sending.size > 0) await Promise.all([...sending]);
1362
+ if (sendError !== void 0) {
1363
+ const err = sendError;
1364
+ sendError = void 0;
1365
+ throw err;
1366
+ }
1367
+ }
1297
1368
  async function flush() {
1369
+ await flushBuffer();
1370
+ await settle();
1371
+ }
1372
+ async function flushBuffer() {
1298
1373
  if (buffer.length === 0) return;
1299
1374
  const pending = buffer;
1300
1375
  buffer = [];
@@ -1302,6 +1377,9 @@ async function startRun(passed, declared) {
1302
1377
  defer(pending);
1303
1378
  return;
1304
1379
  }
1380
+ await dispatch(() => deliver(pending));
1381
+ }
1382
+ async function deliver(pending) {
1305
1383
  await resolveKeys(pending.flatMap((p) => p.keys));
1306
1384
  const submissions = [];
1307
1385
  const unanswered = [];
@@ -1324,6 +1402,9 @@ async function startRun(passed, declared) {
1324
1402
  progress();
1325
1403
  return;
1326
1404
  }
1405
+ await send(submissions, filesByKey);
1406
+ }
1407
+ async function send(submissions, filesByKey) {
1327
1408
  for (const part of packed(submissions)) {
1328
1409
  if (offline) {
1329
1410
  defer(part);
@@ -1339,6 +1420,7 @@ async function startRun(passed, declared) {
1339
1420
  for (const k of told) stats[k] += counts[k];
1340
1421
  progress();
1341
1422
  await upload(out.ok ? out.body?.items : void 0, filesByKey);
1423
+ answered = true;
1342
1424
  continue;
1343
1425
  }
1344
1426
  if (out.ok) {
@@ -1367,7 +1449,10 @@ async function startRun(passed, declared) {
1367
1449
  if (stats.rejected > 0) parts.push(`${stats.rejected} rejected`);
1368
1450
  if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
1369
1451
  if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
1370
- if (stats.created > 0) parts.push(`${stats.created} added as cases (trusted source)`);
1452
+ if (stats.created > 0) {
1453
+ const recorded = createdWithResult === stats.created && createdUndelivered === 0;
1454
+ parts.push(`${stats.created} added as cases (trusted source)${recorded ? " and their results recorded" : ""}`);
1455
+ }
1371
1456
  const kinds = [stats.screenshots, stats.textFiles, stats.otherFiles];
1372
1457
  const sum = (what) => kinds.reduce((n, counts) => n + counts[what], 0);
1373
1458
  if (bucketSeen) {
@@ -1397,6 +1482,7 @@ async function startRun(passed, declared) {
1397
1482
  log(`::warning::${stats.deferred} result(s) were not delivered to Plune \u2014 see the reporting step's log.`);
1398
1483
  }
1399
1484
  }
1485
+ const shardLines = [];
1400
1486
  return {
1401
1487
  get runId() {
1402
1488
  return runId;
@@ -1409,7 +1495,7 @@ async function startRun(passed, declared) {
1409
1495
  },
1410
1496
  async add(result) {
1411
1497
  buffer.push({ ...result, keys: bound(result.keys) });
1412
- if (buffer.length >= batchSize) await flush();
1498
+ if (buffer.length >= batchSize) await flushBuffer();
1413
1499
  },
1414
1500
  flush,
1415
1501
  async finish(reason) {
@@ -1434,15 +1520,43 @@ async function startRun(passed, declared) {
1434
1520
  summarise();
1435
1521
  return frozen;
1436
1522
  },
1437
- async leaveOpen() {
1438
- if (done) return;
1523
+ shardLines,
1524
+ async leaveOpen(opts) {
1525
+ if (done) return void 0;
1439
1526
  done = true;
1440
1527
  await flush();
1441
1528
  await offer();
1442
- if (runId !== null) {
1529
+ let frozen;
1530
+ const after = [];
1531
+ const shard = env.shard;
1532
+ let counted = false;
1533
+ let mismatch;
1534
+ if (runId !== null && shard !== void 0 && !offline) {
1535
+ const out = await client.post(`/v1/runs/${runId}/events`, { event: "finish", shardIndex: shard.index, shardsExpected: shard.total });
1536
+ if (out.ok && typeof out.body?.shardsReported === "number") {
1537
+ counted = true;
1538
+ frozen = out.body.summary ?? void 0;
1539
+ const n = out.body.shardsExpected ?? shard.total;
1540
+ if (out.body.run?.statusEvent === "finished") after.push("plune: run closed");
1541
+ after.push(`plune: shard ${shard.index} of ${shard.total} done \u2014 ${out.body.shardsReported} of ${n} shards reported`);
1542
+ } else if (out.ok || out.kind === "invalid") {
1543
+ log('plune: the platform does not support shard auto-close yet \u2014 the run is left open for "plune run finish".');
1544
+ } else if (out.code === "shards_mismatch") {
1545
+ mismatch = new Error(
1546
+ `PLUNE_SHARD=${shard.index}/${shard.total} says ${shard.total} shards, but run ${runId} already expects ${out.shardsExpected ?? "a different number of"} \u2014 every shard of one run must use the same N. The results are uploaded; the run is left open.`
1547
+ );
1548
+ } else {
1549
+ log(`plune: could not report shard ${shard.index} of ${shard.total} to run ${runId} (${out.detail || out.kind}) \u2014 the results are uploaded; close the run with "plune run finish ${runId}".`);
1550
+ }
1551
+ }
1552
+ if (runId !== null && !counted) {
1443
1553
  log(`plune: run ${runId} is still open \u2014 close it with "plune run finish ${runId}".`);
1444
1554
  }
1445
1555
  summarise();
1556
+ shardLines.push(...after);
1557
+ if (opts?.holdShardLines !== true) for (const line of after) log(line);
1558
+ if (mismatch !== void 0) throw mismatch;
1559
+ return frozen;
1446
1560
  }
1447
1561
  };
1448
1562
  }
@@ -326,7 +326,7 @@ interface ReporterConfig {
326
326
  labels?: string[];
327
327
  /** Results per request. The platform's ceiling is 500. */
328
328
  batchSize?: number;
329
- /** Files in flight at once (`PLUNE_UPLOAD_CONCURRENCY`, 1 to 32, default 4). */
329
+ /** Files in flight at once (`PLUNE_UPLOAD_CONCURRENCY`, 1 to 32, default 16). */
330
330
  uploadConcurrency?: number;
331
331
  /** Retries of a request the platform or a bucket asked to wait for (`PLUNE_UPLOAD_RETRIES`, 0 to 20, default 6). */
332
332
  uploadRetries?: number;
@@ -377,8 +377,15 @@ interface RunSession {
377
377
  * `undefined` when it answered with none: no run, a refused finish, a deployment older than the summary.
378
378
  */
379
379
  finish(reason?: string): Promise<FrozenSummary | undefined>;
380
- /** Send what is left and deliberately leave the run open — the shard case (AC-03). */
381
- leaveOpen(): Promise<void>;
380
+ /**
381
+ * Send what is left and deliberately leave the run open - the shard case (AC-03). With `PLUNE_SHARD=k/N`
382
+ * it reports "shard k of N is done" instead (cli#110) and resolves to the summary when that was the Nth.
383
+ */
384
+ leaveOpen(opts?: {
385
+ holdShardLines?: boolean;
386
+ }): Promise<FrozenSummary | undefined>;
387
+ /** The closing lines of a `PLUNE_SHARD` report (`run closed`, `shard k of N done`). Printed by `leaveOpen` unless it was told to hold them for a caller that has more to say first. */
388
+ readonly shardLines: readonly string[];
382
389
  }
383
390
  declare function startRun(passed: ReporterConfig, declared?: readonly (readonly KeyRef[])[]): Promise<RunSession>;
384
391
 
@@ -424,11 +431,20 @@ declare function resultKey(testId: string, retry: number): string;
424
431
  * put them. `PLUNE_GROUP` stays: a run group is D6's, and a group of one run means nothing.
425
432
  */
426
433
  declare const UNSUPPORTED_VARS: readonly ["PLUNE_GROUP"];
434
+ interface ShardOf {
435
+ /** 1-based. */
436
+ index: number;
437
+ total: number;
438
+ }
427
439
  interface EnvSettings {
428
440
  /** Config fields the environment supplied. Anything passed explicitly outranks these. */
429
441
  config: Partial<ReporterConfig>;
430
442
  /** This process must not close the run — several share it, or the job closes it itself. */
431
443
  keepOpen: boolean;
444
+ /** `PLUNE_SHARD=k/N`: this process is shard k of N, and tells the platform when it is done (cli#110). */
445
+ shard?: ShardOf;
446
+ /** `PLUNE_SHARD` was set to something that is not `k/N`. A configuration error: the caller refuses to start. */
447
+ shardError?: string;
432
448
  /** Variables that were set and have nowhere to land. The caller says so once. */
433
449
  ignored: string[];
434
450
  /** Variables that were set to something unusable and fell back to their default. The caller says so once. */
@@ -562,6 +578,7 @@ type ClientOutcome<T> = {
562
578
  status: number | null;
563
579
  detail: string;
564
580
  code?: string;
581
+ shardsExpected?: number;
565
582
  };
566
583
  /** What a PUT to a bucket carries: a file on disk, streamed at a length known beforehand, or bytes in memory. */
567
584
  type PutSource = {
@@ -612,6 +629,8 @@ interface PlatformClient {
612
629
  * a proxy error page that echoes the request would put a token in a CI log.
613
630
  */
614
631
  del<T>(path: string): Promise<ClientOutcome<T>>;
632
+ /** A GET, through the same retries, classification and token scrubbing. */
633
+ get<T>(path: string): Promise<ClientOutcome<T>>;
615
634
  /**
616
635
  * The bytes to a short-lived link of the customer's bucket (plune-ai/plune#946): exactly the headers the
617
636
  * link came with, no token of ours. Network failures and 5xx wait and try again, up to three times; a 403
@@ -326,7 +326,7 @@ interface ReporterConfig {
326
326
  labels?: string[];
327
327
  /** Results per request. The platform's ceiling is 500. */
328
328
  batchSize?: number;
329
- /** Files in flight at once (`PLUNE_UPLOAD_CONCURRENCY`, 1 to 32, default 4). */
329
+ /** Files in flight at once (`PLUNE_UPLOAD_CONCURRENCY`, 1 to 32, default 16). */
330
330
  uploadConcurrency?: number;
331
331
  /** Retries of a request the platform or a bucket asked to wait for (`PLUNE_UPLOAD_RETRIES`, 0 to 20, default 6). */
332
332
  uploadRetries?: number;
@@ -377,8 +377,15 @@ interface RunSession {
377
377
  * `undefined` when it answered with none: no run, a refused finish, a deployment older than the summary.
378
378
  */
379
379
  finish(reason?: string): Promise<FrozenSummary | undefined>;
380
- /** Send what is left and deliberately leave the run open — the shard case (AC-03). */
381
- leaveOpen(): Promise<void>;
380
+ /**
381
+ * Send what is left and deliberately leave the run open - the shard case (AC-03). With `PLUNE_SHARD=k/N`
382
+ * it reports "shard k of N is done" instead (cli#110) and resolves to the summary when that was the Nth.
383
+ */
384
+ leaveOpen(opts?: {
385
+ holdShardLines?: boolean;
386
+ }): Promise<FrozenSummary | undefined>;
387
+ /** The closing lines of a `PLUNE_SHARD` report (`run closed`, `shard k of N done`). Printed by `leaveOpen` unless it was told to hold them for a caller that has more to say first. */
388
+ readonly shardLines: readonly string[];
382
389
  }
383
390
  declare function startRun(passed: ReporterConfig, declared?: readonly (readonly KeyRef[])[]): Promise<RunSession>;
384
391
 
@@ -424,11 +431,20 @@ declare function resultKey(testId: string, retry: number): string;
424
431
  * put them. `PLUNE_GROUP` stays: a run group is D6's, and a group of one run means nothing.
425
432
  */
426
433
  declare const UNSUPPORTED_VARS: readonly ["PLUNE_GROUP"];
434
+ interface ShardOf {
435
+ /** 1-based. */
436
+ index: number;
437
+ total: number;
438
+ }
427
439
  interface EnvSettings {
428
440
  /** Config fields the environment supplied. Anything passed explicitly outranks these. */
429
441
  config: Partial<ReporterConfig>;
430
442
  /** This process must not close the run — several share it, or the job closes it itself. */
431
443
  keepOpen: boolean;
444
+ /** `PLUNE_SHARD=k/N`: this process is shard k of N, and tells the platform when it is done (cli#110). */
445
+ shard?: ShardOf;
446
+ /** `PLUNE_SHARD` was set to something that is not `k/N`. A configuration error: the caller refuses to start. */
447
+ shardError?: string;
432
448
  /** Variables that were set and have nowhere to land. The caller says so once. */
433
449
  ignored: string[];
434
450
  /** Variables that were set to something unusable and fell back to their default. The caller says so once. */
@@ -562,6 +578,7 @@ type ClientOutcome<T> = {
562
578
  status: number | null;
563
579
  detail: string;
564
580
  code?: string;
581
+ shardsExpected?: number;
565
582
  };
566
583
  /** What a PUT to a bucket carries: a file on disk, streamed at a length known beforehand, or bytes in memory. */
567
584
  type PutSource = {
@@ -612,6 +629,8 @@ interface PlatformClient {
612
629
  * a proxy error page that echoes the request would put a token in a CI log.
613
630
  */
614
631
  del<T>(path: string): Promise<ClientOutcome<T>>;
632
+ /** A GET, through the same retries, classification and token scrubbing. */
633
+ get<T>(path: string): Promise<ClientOutcome<T>>;
615
634
  /**
616
635
  * The bytes to a short-lived link of the customer's bucket (plune-ai/plune#946): exactly the headers the
617
636
  * link came with, no token of ours. Network failures and 5xx wait and try again, up to three times; a 403