@aexhq/sdk 0.40.7 → 0.40.9

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.
package/dist/cli.mjs CHANGED
@@ -2261,6 +2261,8 @@ function extractErrorMessage(body) {
2261
2261
  // ../contracts/dist/operations.js
2262
2262
  var operations_exports = {};
2263
2263
  __export(operations_exports, {
2264
+ OUTPUT_FILE_TRANSFER_ATTEMPTS: () => OUTPUT_FILE_TRANSFER_ATTEMPTS,
2265
+ OUTPUT_FILE_TRANSFER_DEFAULT_TIMEOUT_MS: () => OUTPUT_FILE_TRANSFER_DEFAULT_TIMEOUT_MS,
2264
2266
  READ_OUTPUT_TEXT_DEFAULT_BYTES: () => READ_OUTPUT_TEXT_DEFAULT_BYTES,
2265
2267
  READ_OUTPUT_TEXT_MAX_BYTES: () => READ_OUTPUT_TEXT_MAX_BYTES,
2266
2268
  approveSession: () => approveSession,
@@ -3322,28 +3324,146 @@ function resolveOutputFileSelector(outputs, selector, runId) {
3322
3324
  }
3323
3325
  return { ...selector, id: selector.id };
3324
3326
  }
3325
- async function downloadOutput(http, runId, selector) {
3327
+ async function downloadOutput(http, runId, selector, options) {
3326
3328
  const output = isPathSelector(selector) ? resolveOutputFileSelector(await listOutputs(http, runId), selector, runId) : resolveOutputFileSelector([], selector, runId);
3327
- const { response } = await http.download(`/api/runs/${encodeURIComponent(runId)}/outputs/${encodeURIComponent(output.id)}/download`);
3328
- return { output, bytes: new Uint8Array(await response.arrayBuffer()) };
3329
+ const timeoutMs = normalizeOutputTransferTimeoutMs(options?.timeoutMs);
3330
+ const path = `/api/runs/${encodeURIComponent(runId)}/outputs/${encodeURIComponent(output.id)}/download`;
3331
+ return { output, bytes: await downloadOutputBytesWithRetry(http, path, timeoutMs) };
3329
3332
  }
3330
3333
  var READ_OUTPUT_TEXT_MAX_BYTES = 1e7;
3331
3334
  var READ_OUTPUT_TEXT_DEFAULT_BYTES = 5e4;
3335
+ var OUTPUT_FILE_TRANSFER_DEFAULT_TIMEOUT_MS = 15e3;
3336
+ var OUTPUT_FILE_TRANSFER_ATTEMPTS = 2;
3332
3337
  async function readOutputText(http, runId, selector, options) {
3333
3338
  const maxBytes = Math.max(1, Math.min(options?.maxBytes ?? READ_OUTPUT_TEXT_DEFAULT_BYTES, READ_OUTPUT_TEXT_MAX_BYTES));
3334
3339
  const output = isPathSelector(selector) ? resolveOutputFileSelector(await listOutputs(http, runId), selector, runId) : resolveOutputFileSelector([], selector, runId);
3335
- const { response } = await http.download(`/api/runs/${encodeURIComponent(runId)}/outputs/${encodeURIComponent(output.id)}/download`);
3336
- const capped = await readCappedText(response, maxBytes);
3340
+ const timeoutMs = normalizeOutputTransferTimeoutMs(options?.timeoutMs);
3341
+ const path = `/api/runs/${encodeURIComponent(runId)}/outputs/${encodeURIComponent(output.id)}/download`;
3342
+ const capped = await readOutputTextWithRetry(http, path, maxBytes, timeoutMs);
3337
3343
  const text = options?.grep === void 0 ? capped.text : grepLines(capped.text, options.grep);
3338
3344
  return { output, text, truncated: capped.truncated, totalBytes: capped.totalBytes };
3339
3345
  }
3340
- async function readCappedText(response, maxBytes) {
3346
+ async function downloadOutputBytesWithRetry(http, path, timeoutMs) {
3347
+ return outputTransferWithRetry(path, timeoutMs, async () => {
3348
+ const response = await downloadOutputResponse(http, path, timeoutMs);
3349
+ return readResponseBytes(response, timeoutMs);
3350
+ });
3351
+ }
3352
+ async function readOutputTextWithRetry(http, path, maxBytes, timeoutMs) {
3353
+ return outputTransferWithRetry(path, timeoutMs, async () => {
3354
+ const response = await downloadOutputResponse(http, path, timeoutMs);
3355
+ return readCappedText(response, maxBytes, timeoutMs);
3356
+ });
3357
+ }
3358
+ async function downloadOutputResponse(http, path, timeoutMs) {
3359
+ const controller = new AbortController();
3360
+ const { response } = await withOutputTransferTimeout(http.download(path, { signal: controller.signal }), timeoutMs, () => controller.abort(), "download-open");
3361
+ return response;
3362
+ }
3363
+ async function outputTransferWithRetry(path, timeoutMs, action) {
3364
+ const startedMs = Date.now();
3365
+ let lastTimeout;
3366
+ for (let attempt = 1; attempt <= OUTPUT_FILE_TRANSFER_ATTEMPTS; attempt += 1) {
3367
+ try {
3368
+ return await action();
3369
+ } catch (err2) {
3370
+ if (!(err2 instanceof OutputTransferTimeoutError))
3371
+ throw err2;
3372
+ lastTimeout = err2;
3373
+ }
3374
+ }
3375
+ throw new AexNetworkError({
3376
+ method: "GET",
3377
+ host: "",
3378
+ path,
3379
+ cause: lastTimeout ?? new OutputTransferTimeoutError("unknown", timeoutMs),
3380
+ attempts: OUTPUT_FILE_TRANSFER_ATTEMPTS,
3381
+ elapsedMs: Date.now() - startedMs
3382
+ });
3383
+ }
3384
+ function normalizeOutputTransferTimeoutMs(value) {
3385
+ if (value === void 0)
3386
+ return OUTPUT_FILE_TRANSFER_DEFAULT_TIMEOUT_MS;
3387
+ if (!Number.isFinite(value) || value <= 0) {
3388
+ throw new RunConfigValidationError("outputs.download: timeoutMs must be a positive finite number", {
3389
+ timeoutMs: value
3390
+ });
3391
+ }
3392
+ return Math.max(1, Math.floor(value));
3393
+ }
3394
+ var OutputTransferTimeoutError = class extends Error {
3395
+ code = "ETIMEDOUT";
3396
+ phase;
3397
+ constructor(phase, timeoutMs) {
3398
+ super(`output transfer phase=${phase} timed out after ${timeoutMs}ms`);
3399
+ this.name = "OutputTransferTimeoutError";
3400
+ this.phase = phase;
3401
+ }
3402
+ };
3403
+ async function withOutputTransferTimeout(promise, timeoutMs, abort, phase) {
3404
+ let timedOut = false;
3405
+ let timeout;
3406
+ const timeoutPromise = new Promise((_, reject) => {
3407
+ timeout = setTimeout(() => {
3408
+ timedOut = true;
3409
+ reject(new OutputTransferTimeoutError(phase, timeoutMs));
3410
+ queueMicrotask(() => {
3411
+ try {
3412
+ abort();
3413
+ } catch {
3414
+ }
3415
+ });
3416
+ }, timeoutMs);
3417
+ });
3418
+ try {
3419
+ return await Promise.race([promise, timeoutPromise]);
3420
+ } catch (err2) {
3421
+ if (timedOut && isAbortLikeError(err2))
3422
+ throw new OutputTransferTimeoutError(phase, timeoutMs);
3423
+ throw err2;
3424
+ } finally {
3425
+ if (timeout !== void 0)
3426
+ clearTimeout(timeout);
3427
+ }
3428
+ }
3429
+ function isAbortLikeError(err2) {
3430
+ const name = err2?.name;
3431
+ return name === "AbortError";
3432
+ }
3433
+ async function readResponseBytes(response, timeoutMs) {
3434
+ const body = response.body;
3435
+ if (!body) {
3436
+ const buffer = await withOutputTransferTimeout(response.arrayBuffer(), timeoutMs, () => {
3437
+ }, "body-read");
3438
+ return new Uint8Array(buffer);
3439
+ }
3440
+ const reader = body.getReader();
3441
+ const chunks = [];
3442
+ try {
3443
+ while (true) {
3444
+ const { done, value } = await withOutputTransferTimeout(reader.read(), timeoutMs, () => {
3445
+ void reader.cancel().catch(() => {
3446
+ });
3447
+ }, "body-read");
3448
+ if (done)
3449
+ break;
3450
+ if (value && value.byteLength > 0)
3451
+ chunks.push(value);
3452
+ }
3453
+ } finally {
3454
+ void reader.cancel().catch(() => {
3455
+ });
3456
+ }
3457
+ return concatBytes(chunks);
3458
+ }
3459
+ async function readCappedText(response, maxBytes, timeoutMs) {
3341
3460
  const declaredRaw = response.headers.get("content-length");
3342
3461
  const declared = declaredRaw !== null && /^\d+$/.test(declaredRaw) ? Number(declaredRaw) : void 0;
3343
3462
  const decoder = new TextDecoder("utf-8");
3344
3463
  const body = response.body;
3345
3464
  if (!body) {
3346
- const buf = new Uint8Array(await response.arrayBuffer());
3465
+ const buf = new Uint8Array(await withOutputTransferTimeout(response.arrayBuffer(), timeoutMs, () => {
3466
+ }, "body-read"));
3347
3467
  const total = declared ?? buf.byteLength;
3348
3468
  return {
3349
3469
  text: decoder.decode(buf.subarray(0, maxBytes)),
@@ -3357,7 +3477,10 @@ async function readCappedText(response, maxBytes) {
3357
3477
  let sawMore = false;
3358
3478
  try {
3359
3479
  while (read < maxBytes) {
3360
- const { done, value } = await reader.read();
3480
+ const { done, value } = await withOutputTransferTimeout(reader.read(), timeoutMs, () => {
3481
+ void reader.cancel().catch(() => {
3482
+ });
3483
+ }, "body-read");
3361
3484
  if (done)
3362
3485
  break;
3363
3486
  if (value && value.byteLength > 0) {
@@ -3366,12 +3489,15 @@ async function readCappedText(response, maxBytes) {
3366
3489
  }
3367
3490
  }
3368
3491
  if (read >= maxBytes) {
3369
- const next = await reader.read();
3492
+ const next = await withOutputTransferTimeout(reader.read(), timeoutMs, () => {
3493
+ void reader.cancel().catch(() => {
3494
+ });
3495
+ }, "body-read");
3370
3496
  if (!next.done && next.value && next.value.byteLength > 0)
3371
3497
  sawMore = true;
3372
3498
  }
3373
3499
  } finally {
3374
- await reader.cancel().catch(() => {
3500
+ void reader.cancel().catch(() => {
3375
3501
  });
3376
3502
  }
3377
3503
  const merged = concatBytes(chunks).subarray(0, maxBytes);
@@ -3443,17 +3569,17 @@ async function getBillingLedger(http, query) {
3443
3569
  async function getWebhookSigningSecret(http) {
3444
3570
  return http.request("/api/webhook/signing-secret", { method: "POST" });
3445
3571
  }
3446
- async function collectArtifactBytes(http, runId, items, zipPrefix, namespace2) {
3572
+ async function collectArtifactBytes(http, runId, items, zipPrefix, namespace2, timeoutMs = OUTPUT_FILE_TRANSFER_DEFAULT_TIMEOUT_MS) {
3447
3573
  const entries = [];
3448
3574
  const captured = [];
3449
3575
  const errors = [];
3450
3576
  for (const item of items) {
3451
3577
  const rel = item.filename ?? item.id;
3452
3578
  try {
3453
- const { response } = await http.download(`/api/runs/${encodeURIComponent(runId)}/${namespace2}/${encodeURIComponent(item.id)}/download`);
3579
+ const path = `/api/runs/${encodeURIComponent(runId)}/${namespace2}/${encodeURIComponent(item.id)}/download`;
3454
3580
  entries.push({
3455
3581
  path: `${zipPrefix}${rel}`,
3456
- bytes: new Uint8Array(await response.arrayBuffer()),
3582
+ bytes: await downloadOutputBytesWithRetry(http, path, timeoutMs),
3457
3583
  ...item.contentType !== void 0 ? { contentType: item.contentType } : {},
3458
3584
  customerContent: true
3459
3585
  });
@@ -3679,9 +3805,10 @@ async function download(http, runId) {
3679
3805
  jsonEntry("manifest.json", manifest)
3680
3806
  ]);
3681
3807
  }
3682
- async function downloadOutputs(http, runId) {
3808
+ async function downloadOutputs(http, runId, options) {
3683
3809
  const outputs = await listOutputs(http, runId);
3684
- const { entries, captured, errors } = await collectArtifactBytes(http, runId, outputs, "", "outputs");
3810
+ const timeoutMs = normalizeOutputTransferTimeoutMs(options?.timeoutMs);
3811
+ const { entries, captured, errors } = await collectArtifactBytes(http, runId, outputs, "", "outputs", timeoutMs);
3685
3812
  return zipEntries([
3686
3813
  ...entries,
3687
3814
  jsonEntry("manifest.json", { runId, namespace: "outputs", outputs: captured, errors })
@@ -4456,91 +4583,119 @@ function renderVerbHelp(spec) {
4456
4583
  // dist/host/run-cmd.js
4457
4584
  import { resolve as resolvePath } from "node:path";
4458
4585
 
4459
- // ../contracts/dist/post-hook.js
4460
- var DEFAULT_POST_HOOK_TIMEOUT_MS = 5 * 60 * 1e3;
4461
-
4462
- // ../contracts/dist/internal.js
4463
- var DIRECT_UPLOAD_MAX_ATTEMPTS = 3;
4464
- async function uploadAsset(args) {
4465
- const expected = args.hash.startsWith("sha256:") ? args.hash.slice("sha256:".length) : args.hash;
4466
- const actual = await computeSha256Hex(args.bytes);
4467
- if (actual !== expected) {
4468
- throw new Error(`uploadAsset: client-side hash mismatch: computed sha256:${actual} but caller declared ${args.hash}. Aborting to avoid uploading corrupted data.`);
4469
- }
4470
- const contentHashHeader = `sha256:${actual}`;
4471
- const presign = await args.http.request("/assets/presign", {
4472
- method: "POST",
4473
- headers: { "content-type": "application/json" },
4474
- body: JSON.stringify({ hash: contentHashHeader, sizeBytes: args.bytes.byteLength })
4475
- });
4476
- if (presign.exists) {
4477
- const contentHash2 = presign.contentHash ?? contentHashHeader;
4478
- return {
4479
- assetId: presign.assetId ?? assetIdFromContentHash(contentHash2),
4480
- contentHash: contentHash2,
4481
- sizeBytes: presign.sizeBytes ?? args.bytes.byteLength,
4482
- exists: true
4483
- };
4586
+ // ../contracts/dist/retry-core.js
4587
+ var RETRYABLE_HTTP_STATUS = [429, 500, 502, 503, 504, 529];
4588
+ var RATE_LIMIT_HTTP_STATUS = [429, 503, 529];
4589
+ var RETRYABLE_HTTP_STATUS_SET = new Set(RETRYABLE_HTTP_STATUS);
4590
+ var RATE_LIMIT_HTTP_STATUS_SET = new Set(RATE_LIMIT_HTTP_STATUS);
4591
+ function parseRetryAfterMs(headerValue, now = Date.now()) {
4592
+ if (headerValue === null || headerValue === void 0)
4593
+ return void 0;
4594
+ const trimmed = headerValue.trim();
4595
+ if (trimmed.length === 0)
4596
+ return void 0;
4597
+ if (/^\d+$/.test(trimmed)) {
4598
+ return Number(trimmed) * 1e3;
4484
4599
  }
4485
- if (!presign.uploadUrl) {
4486
- throw new Error("uploadAsset: presign returned no uploadUrl and exists:false");
4600
+ const dateMs = Date.parse(trimmed);
4601
+ if (!Number.isNaN(dateMs)) {
4602
+ return Math.max(0, dateMs - now);
4487
4603
  }
4488
- const doFetch = args.fetch ?? globalThis.fetch;
4489
- const putHeaders = {
4490
- "content-type": args.contentType ?? "application/zip",
4491
- ...presign.requiredHeaders ?? {}
4492
- };
4493
- await putWithRetry(doFetch, presign.uploadUrl, {
4494
- method: "PUT",
4495
- headers: putHeaders,
4496
- body: args.bytes
4497
- });
4498
- const fin = await args.http.request("/assets/finalize", {
4499
- method: "POST",
4500
- headers: { "content-type": "application/json" },
4501
- body: JSON.stringify({ hash: contentHashHeader, sizeBytes: args.bytes.byteLength })
4604
+ return void 0;
4605
+ }
4606
+ function computeRetryBackoffDelayMs(config, attemptNumber, random) {
4607
+ const exponent = Math.max(0, attemptNumber - 1);
4608
+ const nominal = Math.min(config.maxDelayMs, config.initialDelayMs * 2 ** exponent);
4609
+ return Math.round(random() * nominal);
4610
+ }
4611
+ function computeRetryDelayMs(config, attemptNumber, random, retryAfterMs) {
4612
+ const backoff = computeRetryBackoffDelayMs(config, attemptNumber, random);
4613
+ return retryAfterMs === void 0 ? backoff : Math.max(retryAfterMs, backoff);
4614
+ }
4615
+ function abortableSleep(ms, signal) {
4616
+ return new Promise((resolve, reject) => {
4617
+ if (signal?.aborted) {
4618
+ reject(abortReason(signal));
4619
+ return;
4620
+ }
4621
+ const timer = setTimeout(() => {
4622
+ signal?.removeEventListener("abort", onAbort);
4623
+ resolve();
4624
+ }, ms);
4625
+ const onAbort = () => {
4626
+ clearTimeout(timer);
4627
+ reject(signal ? abortReason(signal) : makeAbortError());
4628
+ };
4629
+ signal?.addEventListener("abort", onAbort, { once: true });
4502
4630
  });
4503
- const contentHash = fin.contentHash ?? presign.contentHash ?? contentHashHeader;
4504
- return {
4505
- assetId: fin.assetId ?? presign.assetId ?? assetIdFromContentHash(contentHash),
4506
- contentHash,
4507
- sizeBytes: fin.sizeBytes ?? args.bytes.byteLength,
4508
- exists: false
4509
- };
4510
4631
  }
4511
- async function computeSha256Hex(bytes) {
4512
- const subtle = globalThis.crypto?.subtle;
4513
- if (!subtle) {
4514
- throw new Error("uploadAsset: globalThis.crypto.subtle is not available; Bun, Node 18+, or a Web-Crypto-capable runtime is required");
4515
- }
4516
- const digest = await subtle.digest("SHA-256", bytes);
4517
- return bufferToHex(digest);
4632
+ function abortReason(signal) {
4633
+ return signal.reason instanceof Error ? signal.reason : makeAbortError();
4518
4634
  }
4519
- function assetIdFromContentHash(contentHash) {
4520
- const hex = contentHash.startsWith("sha256:") ? contentHash.slice("sha256:".length) : contentHash;
4521
- return `asset_${hex}`;
4635
+ function makeAbortError() {
4636
+ if (typeof DOMException !== "undefined")
4637
+ return new DOMException("Aborted", "AbortError");
4638
+ const err2 = new Error("Aborted");
4639
+ err2.name = "AbortError";
4640
+ return err2;
4522
4641
  }
4523
- async function putWithRetry(fetchImpl, uploadUrl, init) {
4524
- for (let attempt = 1; attempt <= DIRECT_UPLOAD_MAX_ATTEMPTS; attempt++) {
4642
+
4643
+ // ../contracts/dist/asset-upload-helper.js
4644
+ var DIRECT_UPLOAD_MAX_ATTEMPTS = 3;
4645
+ var DIRECT_UPLOAD_INITIAL_DELAY_MS = 500;
4646
+ var DIRECT_UPLOAD_MAX_DELAY_MS = 5e3;
4647
+ var DIRECT_UPLOAD_MAX_ELAPSED_MS = 3e4;
4648
+ async function putDirectUploadWithRetry(fetchImpl, uploadUrl, init, options = {}) {
4649
+ const config = resolveAssetUploadRetryConfig(options);
4650
+ const sleep5 = options.sleep ?? abortableSleep;
4651
+ const random = options.random ?? Math.random;
4652
+ const now = options.now ?? Date.now;
4653
+ const startedAt = now();
4654
+ for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
4525
4655
  let response;
4526
4656
  try {
4527
4657
  response = await fetchImpl(uploadUrl, init);
4528
4658
  } catch (err2) {
4529
- if (attempt < DIRECT_UPLOAD_MAX_ATTEMPTS && isRetryableUploadError(err2)) {
4659
+ if (attempt < config.maxAttempts && isRetryableUploadError(err2)) {
4660
+ const delay = directUploadRetryDelayMs(config, attempt, random, void 0);
4661
+ if (!withinDirectUploadRetryBudget(config, startedAt, delay, now)) {
4662
+ throw directUploadNetworkError(uploadUrl, err2, attempt);
4663
+ }
4664
+ await sleep5(delay, init.signal ?? void 0);
4530
4665
  continue;
4531
4666
  }
4532
4667
  throw directUploadNetworkError(uploadUrl, err2, attempt);
4533
4668
  }
4534
4669
  if (response.ok)
4535
4670
  return;
4536
- if (attempt < DIRECT_UPLOAD_MAX_ATTEMPTS && isRetryableUploadStatus(response.status)) {
4671
+ const retryAfterMs = parseRetryAfterMs(response.headers?.get("retry-after"), now());
4672
+ if (attempt < config.maxAttempts && isRetryableUploadStatus(response.status)) {
4673
+ const delay = directUploadRetryDelayMs(config, attempt, random, retryAfterMs);
4674
+ if (!withinDirectUploadRetryBudget(config, startedAt, delay, now)) {
4675
+ const detail2 = await response.text().catch(() => "");
4676
+ throw directUploadResponseError(uploadUrl, response.status, detail2, attempt);
4677
+ }
4537
4678
  await response.text().catch(() => "");
4679
+ await sleep5(delay, init.signal ?? void 0);
4538
4680
  continue;
4539
4681
  }
4540
4682
  const detail = await response.text().catch(() => "");
4541
4683
  throw directUploadResponseError(uploadUrl, response.status, detail, attempt);
4542
4684
  }
4543
4685
  }
4686
+ function resolveAssetUploadRetryConfig(options = {}) {
4687
+ const maxAttempts = Math.max(1, Math.floor(options.maxAttempts ?? DIRECT_UPLOAD_MAX_ATTEMPTS));
4688
+ const initialDelayMs = Math.max(0, options.initialDelayMs ?? DIRECT_UPLOAD_INITIAL_DELAY_MS);
4689
+ const maxDelayMs = Math.max(initialDelayMs, options.maxDelayMs ?? DIRECT_UPLOAD_MAX_DELAY_MS);
4690
+ const maxElapsedMs = Math.max(0, options.maxElapsedMs ?? DIRECT_UPLOAD_MAX_ELAPSED_MS);
4691
+ return { maxAttempts, initialDelayMs, maxDelayMs, maxElapsedMs };
4692
+ }
4693
+ function directUploadRetryDelayMs(config, attempt, random, retryAfterMs) {
4694
+ return computeRetryDelayMs(config, attempt, random, retryAfterMs);
4695
+ }
4696
+ function withinDirectUploadRetryBudget(config, startedAt, delayMs, now) {
4697
+ return now() - startedAt + delayMs <= config.maxElapsedMs;
4698
+ }
4544
4699
  function isRetryableUploadStatus(status) {
4545
4700
  return status === 408 || status === 425 || status === 429 || status >= 500 && status <= 599;
4546
4701
  }
@@ -4560,6 +4715,9 @@ function directUploadResponseError(uploadUrl, status, detail, attempts) {
4560
4715
  const safeDetail = sanitizeUploadText(detail).slice(0, 500);
4561
4716
  return new Error(`uploadAsset: direct upload PUT failed for ${safeUrl} with status ${status}` + (attempts > 1 ? ` after ${attemptsLabel(attempts)}` : "") + (safeDetail ? `: ${safeDetail}` : ""));
4562
4717
  }
4718
+ function sanitizeUploadText(text) {
4719
+ return text.replace(/https?:\/\/[^\s<>"'`]+/g, (raw) => redactUrlPreservingTrailingPunctuation(raw)).replace(/\b(?:X-Amz-(?:Algorithm|Credential|Date|Expires|Security-Token|Signature|SignedHeaders)|AWSAccessKeyId|Signature|Credential|Security-Token|AccessKeyId|SecretAccessKey|SessionToken)=([^&\s<>"'`]+)/gi, "[redacted]").replace(/\bAKIA[0-9A-Z]{8,}\b/g, "[redacted]");
4720
+ }
4563
4721
  function attemptsLabel(attempts) {
4564
4722
  return attempts === 1 ? "1 attempt" : `${attempts} attempts`;
4565
4723
  }
@@ -4579,14 +4737,75 @@ function stringProperty2(value, key) {
4579
4737
  const prop = value[key];
4580
4738
  return typeof prop === "string" && prop.length > 0 ? prop : void 0;
4581
4739
  }
4582
- function sanitizeUploadText(text) {
4583
- return text.replace(/https?:\/\/[^\s<>"'`]+/g, (raw) => redactUrlPreservingTrailingPunctuation(raw)).replace(/\b(?:X-Amz-(?:Algorithm|Credential|Date|Expires|Security-Token|Signature|SignedHeaders)|AWSAccessKeyId|Signature|Credential|Security-Token|AccessKeyId|SecretAccessKey|SessionToken)=([^&\s<>"'`]+)/gi, "[redacted]").replace(/\bAKIA[0-9A-Z]{8,}\b/g, "[redacted]");
4584
- }
4585
4740
  function redactUrlPreservingTrailingPunctuation(raw) {
4586
4741
  const trailing = raw.match(/[),.;:!?]+$/)?.[0] ?? "";
4587
4742
  const candidate = trailing ? raw.slice(0, -trailing.length) : raw;
4588
4743
  return `${redactUrl(candidate)}${trailing}`;
4589
4744
  }
4745
+
4746
+ // ../contracts/dist/post-hook.js
4747
+ var DEFAULT_POST_HOOK_TIMEOUT_MS = 5 * 60 * 1e3;
4748
+
4749
+ // ../contracts/dist/internal.js
4750
+ async function uploadAsset(args) {
4751
+ const expected = args.hash.startsWith("sha256:") ? args.hash.slice("sha256:".length) : args.hash;
4752
+ const actual = await computeSha256Hex(args.bytes);
4753
+ if (actual !== expected) {
4754
+ throw new Error(`uploadAsset: client-side hash mismatch: computed sha256:${actual} but caller declared ${args.hash}. Aborting to avoid uploading corrupted data.`);
4755
+ }
4756
+ const contentHashHeader = `sha256:${actual}`;
4757
+ const presign = await args.http.request("/assets/presign", {
4758
+ method: "POST",
4759
+ headers: { "content-type": "application/json" },
4760
+ body: JSON.stringify({ hash: contentHashHeader, sizeBytes: args.bytes.byteLength })
4761
+ });
4762
+ if (presign.exists) {
4763
+ const contentHash2 = presign.contentHash ?? contentHashHeader;
4764
+ return {
4765
+ assetId: presign.assetId ?? assetIdFromContentHash(contentHash2),
4766
+ contentHash: contentHash2,
4767
+ sizeBytes: presign.sizeBytes ?? args.bytes.byteLength,
4768
+ exists: true
4769
+ };
4770
+ }
4771
+ if (!presign.uploadUrl) {
4772
+ throw new Error("uploadAsset: presign returned no uploadUrl and exists:false");
4773
+ }
4774
+ const doFetch = args.fetch ?? globalThis.fetch;
4775
+ const putHeaders = {
4776
+ "content-type": args.contentType ?? "application/zip",
4777
+ ...presign.requiredHeaders ?? {}
4778
+ };
4779
+ await putDirectUploadWithRetry(doFetch, presign.uploadUrl, {
4780
+ method: "PUT",
4781
+ headers: putHeaders,
4782
+ body: args.bytes
4783
+ }, args.retry);
4784
+ const fin = await args.http.request("/assets/finalize", {
4785
+ method: "POST",
4786
+ headers: { "content-type": "application/json" },
4787
+ body: JSON.stringify({ hash: contentHashHeader, sizeBytes: args.bytes.byteLength })
4788
+ });
4789
+ const contentHash = fin.contentHash ?? presign.contentHash ?? contentHashHeader;
4790
+ return {
4791
+ assetId: fin.assetId ?? presign.assetId ?? assetIdFromContentHash(contentHash),
4792
+ contentHash,
4793
+ sizeBytes: fin.sizeBytes ?? args.bytes.byteLength,
4794
+ exists: false
4795
+ };
4796
+ }
4797
+ async function computeSha256Hex(bytes) {
4798
+ const subtle = globalThis.crypto?.subtle;
4799
+ if (!subtle) {
4800
+ throw new Error("uploadAsset: globalThis.crypto.subtle is not available; Bun, Node 18+, or a Web-Crypto-capable runtime is required");
4801
+ }
4802
+ const digest = await subtle.digest("SHA-256", bytes);
4803
+ return bufferToHex(digest);
4804
+ }
4805
+ function assetIdFromContentHash(contentHash) {
4806
+ const hex = contentHash.startsWith("sha256:") ? contentHash.slice("sha256:".length) : contentHash;
4807
+ return `asset_${hex}`;
4808
+ }
4590
4809
  function bufferToHex(buffer) {
4591
4810
  const view = new Uint8Array(buffer);
4592
4811
  let out = "";
@@ -1 +1 @@
1
- a67b2cbba2818c579806f112ebfbfddb975b028d0950381985e0e6aa73d259ee cli.mjs
1
+ eb3b73297a9e0ef272eefa66f60dc4a13da741cb24a809c7558de2ee5d4d3555 cli.mjs
package/dist/client.d.ts CHANGED
@@ -555,6 +555,11 @@ export type OutputFileSelector = Output | OutputFileIdSelector | OutputFilePathS
555
555
  export type OutputLinkSelector = string | OutputFileSelector | OutputQuery;
556
556
  export interface OutputDownloadOptions {
557
557
  readonly to?: string;
558
+ /**
559
+ * Per-attempt timeout for fetching and reading the selected output body.
560
+ * Defaults to 15_000ms; idempotent output downloads retry once on timeout.
561
+ */
562
+ readonly timeoutMs?: number;
558
563
  }
559
564
  /**
560
565
  * Workspace AgentsMd admin operations exposed under `client.agentsMd`.
package/dist/client.js CHANGED
@@ -746,9 +746,10 @@ function isSessionEnvelopeTerminal(event) {
746
746
  async function downloadSessionOutput(http, id, selector, options) {
747
747
  // One selector-resolution path: the contracts `downloadOutput` lists-if-path
748
748
  // then downloads, throwing with PUBLIC verb names — no duplicated resolver.
749
+ const transferOptions = options?.timeoutMs === undefined ? undefined : { timeoutMs: options.timeoutMs };
749
750
  const bytes = selector === undefined
750
- ? await operations.downloadOutputs(http, id)
751
- : (await operations.downloadOutput(http, id, selector)).bytes;
751
+ ? await operations.downloadOutputs(http, id, transferOptions)
752
+ : (await operations.downloadOutput(http, id, selector, transferOptions)).bytes;
752
753
  return writeOptionalFile(bytes, options?.to);
753
754
  }
754
755
  /**