@miosa/sdk 3.2.3 → 3.2.5

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/index.js CHANGED
@@ -18,7 +18,15 @@ var MiosaError = class _MiosaError extends Error {
18
18
  details;
19
19
  requestId;
20
20
  retryable;
21
- constructor(message, status, code, details, requestId, retryable = defaultRetryable(status)) {
21
+ /**
22
+ * Server-supplied retry hint in milliseconds, taken from the canonical
23
+ * error shape's `retry_after_ms` (nested under `error` or top-level) or,
24
+ * failing that, the HTTP `Retry-After` header. `undefined` when the
25
+ * response carried neither, in which case a caller should fall back to
26
+ * its own backoff policy.
27
+ */
28
+ retryAfterMs;
29
+ constructor(message, status, code, details, requestId, retryable = defaultRetryable(status), retryAfterMs) {
22
30
  super(message);
23
31
  this.name = "MiosaError";
24
32
  this.status = status;
@@ -26,34 +34,33 @@ var MiosaError = class _MiosaError extends Error {
26
34
  this.details = details;
27
35
  this.requestId = requestId;
28
36
  this.retryable = retryable;
37
+ this.retryAfterMs = retryAfterMs;
29
38
  }
30
- static fromResponse(status, body5, requestId) {
39
+ /**
40
+ * @param retryAfterHeaderMs The `Retry-After` header value, already
41
+ * converted to milliseconds by the caller. Used only when the body does
42
+ * not carry the more precise `retry_after_ms`.
43
+ */
44
+ static fromResponse(status, body5, requestId, retryAfterHeaderMs2) {
31
45
  const nested = typeof body5.error === "object" ? body5.error : void 0;
32
46
  const flatError = typeof body5.error === "string" ? body5.error : void 0;
33
47
  const flatErrorIsCode = !!flatError && /^[A-Z][A-Z0-9_]+$/.test(flatError);
34
48
  const message = nested?.message ?? body5.message ?? body5.detail ?? body5.reason ?? flatError ?? `HTTP ${status}`;
35
49
  const code = nested?.code ?? body5.code ?? (flatErrorIsCode ? flatError : "UNKNOWN_ERROR");
36
50
  const details = nested?.details ?? body5.details ?? (body5.detail || body5.reason ? { detail: body5.detail, reason: body5.reason } : void 0);
37
- requestId ??= body5.request_id;
51
+ requestId ??= nested?.request_id ?? body5.request_id;
38
52
  const retryable = nested?.retryable ?? body5.retryable ?? defaultRetryable(status);
53
+ const retryAfterMs = nested?.retry_after_ms ?? body5.retry_after_ms ?? retryAfterHeaderMs2;
39
54
  const connectError = connectErrorFromCode(code, message, status, details, requestId, retryable);
40
- if (connectError) return connectError;
41
- if (status === 401 || status === 403) {
42
- return new AuthError(message, status, code, details, requestId, retryable);
43
- }
44
- if (status === 404) {
45
- return new NotFoundError(message, code, details, requestId, retryable);
46
- }
47
- if (status === 429) {
48
- return new RateLimitError(message, details, requestId, void 0, retryable);
49
- }
50
- if (status === 402) {
51
- return new InsufficientCreditsError(message, details, requestId, retryable);
52
- }
53
- if (status >= 400 && status < 500) {
54
- return new ValidationError(message, status, code, details, requestId, retryable);
55
- }
56
- return new _MiosaError(message, status, code, details, requestId, retryable);
55
+ const built = connectError ?? (status === 401 || status === 403 ? new AuthError(message, status, code, details, requestId, retryable) : status === 404 ? new NotFoundError(message, code, details, requestId, retryable) : status === 429 ? new RateLimitError(
56
+ message,
57
+ details,
58
+ requestId,
59
+ retryAfterMs === void 0 ? void 0 : retryAfterMs / 1e3,
60
+ retryable
61
+ ) : status === 402 ? new InsufficientCreditsError(message, details, requestId, retryable) : status >= 400 && status < 500 ? new ValidationError(message, status, code, details, requestId, retryable) : new _MiosaError(message, status, code, details, requestId, retryable));
62
+ built.retryAfterMs = retryAfterMs;
63
+ return built;
57
64
  }
58
65
  };
59
66
  function connectErrorFromCode(code, message, status, details, requestId, retryable) {
@@ -174,7 +181,7 @@ var TokenRefreshFailedError = class extends MiosaError {
174
181
  };
175
182
 
176
183
  // src/version.ts
177
- var SDK_VERSION = "3.2.3";
184
+ var SDK_VERSION = "3.2.5";
178
185
  var SDK_USER_AGENT = `@miosa/sdk/${SDK_VERSION}`;
179
186
 
180
187
  // src/http.ts
@@ -206,15 +213,18 @@ async function ensureHttp2Agent() {
206
213
  ensureHttp2Agent();
207
214
  var DEFAULT_TIMEOUT = 3e4;
208
215
  var DEFAULT_MAX_RETRIES = 3;
209
- var RETRY_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
210
216
  var BASE_DELAY_MS = 500;
211
217
  function sleep(ms) {
212
218
  return new Promise((resolve) => setTimeout(resolve, ms));
213
219
  }
214
- function retryDelay(attempt, retryAfterHeader) {
215
- if (retryAfterHeader !== null) {
216
- const seconds = parseInt(retryAfterHeader, 10);
217
- if (!isNaN(seconds)) return seconds * 1e3;
220
+ function retryAfterHeaderMs(retryAfterHeader) {
221
+ if (retryAfterHeader === null) return void 0;
222
+ const seconds = parseInt(retryAfterHeader, 10);
223
+ return isNaN(seconds) ? void 0 : seconds * 1e3;
224
+ }
225
+ function retryDelay(attempt, hintMs) {
226
+ if (hintMs !== void 0 && Number.isFinite(hintMs) && hintMs >= 0) {
227
+ return hintMs;
218
228
  }
219
229
  return BASE_DELAY_MS * 2 ** attempt + Math.random() * 100;
220
230
  }
@@ -289,7 +299,9 @@ var HttpClient = class {
289
299
  clearTimeout(timer);
290
300
  if (!response.ok) {
291
301
  const requestId = response.headers.get("x-request-id") ?? void 0;
292
- const retryAfter = response.headers.get("retry-after");
302
+ const retryAfterMs = retryAfterHeaderMs(
303
+ response.headers.get("retry-after")
304
+ );
293
305
  let errorBody = {};
294
306
  try {
295
307
  errorBody = await response.json();
@@ -298,11 +310,12 @@ var HttpClient = class {
298
310
  const error = MiosaError.fromResponse(
299
311
  response.status,
300
312
  errorBody,
301
- requestId
313
+ requestId,
314
+ retryAfterMs
302
315
  );
303
- if (RETRY_STATUS.has(response.status) && attempt < maxRetries) {
316
+ if (error.retryable && attempt < maxRetries) {
304
317
  lastError = error;
305
- await sleep(retryDelay(attempt, retryAfter));
318
+ await sleep(retryDelay(attempt, error.retryAfterMs));
306
319
  continue;
307
320
  }
308
321
  throw error;
@@ -333,7 +346,7 @@ var HttpClient = class {
333
346
  );
334
347
  if (attempt < maxRetries) {
335
348
  lastError = networkErr;
336
- await sleep(retryDelay(attempt, null));
349
+ await sleep(retryDelay(attempt, void 0));
337
350
  continue;
338
351
  }
339
352
  throw networkErr;
@@ -437,7 +450,8 @@ var HttpClient = class {
437
450
  throw MiosaError.fromResponse(
438
451
  response.status,
439
452
  errorBody,
440
- response.headers.get("x-request-id") ?? void 0
453
+ response.headers.get("x-request-id") ?? void 0,
454
+ retryAfterHeaderMs(response.headers.get("retry-after"))
441
455
  );
442
456
  }
443
457
  if (!response.body) {
@@ -8517,6 +8531,7 @@ function isCommandReady(payload) {
8517
8531
  return status === "ready";
8518
8532
  });
8519
8533
  }
8534
+ var READINESS_STREAM_MAX_TIMEOUT_SEC = 120;
8520
8535
  var SANDBOX_TEMPLATE = "miosa-sandbox";
8521
8536
  var SANDBOX_TIER_BY_CPU = {
8522
8537
  1: { size: "xs", memoryMb: 2048 },
@@ -8651,16 +8666,29 @@ function raiseUnresolvableShape(cpuCount, memoryMb, tier) {
8651
8666
  `Unsupported cpu/memory combination (${supplied}). Supported tiers: xs (1/2048), small (2/4096), medium (4/8192), large (8/16384), xl (16/32768). Pass a matching cpuCount + memoryMb (any diskSizeMb is allowed), use size, or supply cpuCount, memoryMb, and diskSizeMb together for a custom shape.`
8652
8667
  );
8653
8668
  }
8654
- function execDeadline(options) {
8669
+ function validatedExecSeconds(options) {
8655
8670
  const seconds = options?.timeout ?? options?.timeoutSec ?? options?.timeout_sec;
8656
- if (seconds === void 0) return {};
8671
+ if (seconds === void 0) return void 0;
8657
8672
  if (!Number.isFinite(seconds) || seconds <= 0 || seconds > 86400) {
8658
8673
  throw new RangeError(
8659
8674
  "Exec timeout must be greater than zero and at most 86400 seconds"
8660
8675
  );
8661
8676
  }
8677
+ return seconds;
8678
+ }
8679
+ function execDeadline(options) {
8680
+ const seconds = validatedExecSeconds(options);
8681
+ if (seconds === void 0) return {};
8662
8682
  return { timeout: seconds * 1e3 + 5e3 };
8663
8683
  }
8684
+ var DEFAULT_EXEC_WAKE_BUDGET_SEC = 60;
8685
+ function execStreamDeadline(options) {
8686
+ const seconds = validatedExecSeconds(options);
8687
+ if (seconds === void 0) return {};
8688
+ const wakeSeconds = options?.wakeTimeoutSec ?? options?.wake_timeout_sec;
8689
+ const wakeBudgetSec = wakeSeconds !== void 0 ? Math.max(0, wakeSeconds) : DEFAULT_EXEC_WAKE_BUDGET_SEC;
8690
+ return { timeout: (seconds + wakeBudgetSec) * 1e3 + 5e3 };
8691
+ }
8664
8692
  function execBody(command, options = {}) {
8665
8693
  return stripUndefined26({
8666
8694
  command,
@@ -8676,7 +8704,14 @@ function normalizeExecEvent(event, payload) {
8676
8704
  const code = Number(
8677
8705
  record.exit_code ?? record.exitCode ?? (typeof payload === "number" ? payload : 0)
8678
8706
  );
8679
- return { type: "exit", exit_code: code, exitCode: code };
8707
+ const timedOutRaw = record.timed_out ?? record.timedOut;
8708
+ const timedOut = typeof timedOutRaw === "boolean" ? timedOutRaw : void 0;
8709
+ return {
8710
+ type: "exit",
8711
+ exit_code: code,
8712
+ exitCode: code,
8713
+ ...timedOut !== void 0 ? { timed_out: timedOut, timedOut } : {}
8714
+ };
8680
8715
  }
8681
8716
  const type = event === "stderr" ? "stderr" : "stdout";
8682
8717
  const data = typeof payload === "string" ? payload : String(record.line ?? record.data ?? "");
@@ -9144,12 +9179,18 @@ var Sandbox = class _Sandbox {
9144
9179
  result.durationMs = response.duration_ms;
9145
9180
  result.duration_ms = response.duration_ms;
9146
9181
  }
9182
+ const timedOutRaw = response.timed_out ?? response.timedOut;
9183
+ if (typeof timedOutRaw === "boolean") {
9184
+ result.timedOut = timedOutRaw;
9185
+ result.timed_out = timedOutRaw;
9186
+ }
9147
9187
  return result;
9148
9188
  }
9149
9189
  async runCancellableExec(command, options) {
9150
9190
  let stdout = "";
9151
9191
  let stderr = "";
9152
9192
  let exitCode;
9193
+ let timedOut;
9153
9194
  for await (const event of this.execStream(command, options)) {
9154
9195
  if (event.type === "stdout" && typeof event.line === "string")
9155
9196
  stdout += event.line;
@@ -9157,6 +9198,7 @@ var Sandbox = class _Sandbox {
9157
9198
  stderr += event.line;
9158
9199
  if (event.type === "exit" && typeof event.exit_code === "number") {
9159
9200
  exitCode = event.exit_code;
9201
+ timedOut = event.timed_out;
9160
9202
  break;
9161
9203
  }
9162
9204
  }
@@ -9171,7 +9213,13 @@ var Sandbox = class _Sandbox {
9171
9213
  false
9172
9214
  );
9173
9215
  }
9174
- return { stdout, stderr, exitCode, exit_code: exitCode };
9216
+ return {
9217
+ stdout,
9218
+ stderr,
9219
+ exitCode,
9220
+ exit_code: exitCode,
9221
+ ...timedOut !== void 0 ? { timedOut, timed_out: timedOut } : {}
9222
+ };
9175
9223
  }
9176
9224
  execStream(command, options) {
9177
9225
  this.assertRunning("exec.stream");
@@ -9181,7 +9229,7 @@ var Sandbox = class _Sandbox {
9181
9229
  method: "POST",
9182
9230
  body: execBody(command, options),
9183
9231
  signal: options?.signal,
9184
- ...execDeadline(options)
9232
+ ...execStreamDeadline(options)
9185
9233
  }
9186
9234
  );
9187
9235
  return (async function* () {
@@ -9455,17 +9503,26 @@ var Sandbox = class _Sandbox {
9455
9503
  this.data = { ...this.data, ...data };
9456
9504
  return this;
9457
9505
  }
9506
+ /**
9507
+ * Resume a paused (or persistent-stopped) sandbox.
9508
+ *
9509
+ * Always sends an `Idempotency-Key`, generating a fresh one when the
9510
+ * caller doesn't supply one - same rationale as {@link generateIdempotencyKey}
9511
+ * for create(): the key is built once here and threaded through a single
9512
+ * `HttpClient.request` call, whose headers are fixed before its retry loop
9513
+ * runs, so every retry of *this* resume replays the same key. Without this,
9514
+ * a client-side retry of a slow resume could dispatch a second full resume
9515
+ * execution (re-running host reservation) instead of deduping server-side.
9516
+ */
9458
9517
  async resume(idempotencyKey11) {
9459
- const response = idempotencyKey11 ? await this.http.request(
9518
+ const key = idempotencyKey11 ?? generateIdempotencyKey();
9519
+ const response = await this.http.request(
9460
9520
  `/sandboxes/${this.id}/resume`,
9461
9521
  {
9462
9522
  method: "POST",
9463
9523
  body: {},
9464
- headers: { "Idempotency-Key": idempotencyKey11 }
9524
+ headers: { "Idempotency-Key": key }
9465
9525
  }
9466
- ) : await this.http.post(
9467
- `/sandboxes/${this.id}/resume`,
9468
- {}
9469
9526
  );
9470
9527
  const data = unwrap48(response);
9471
9528
  this.data = { ...this.data, ...data };
@@ -9565,47 +9622,66 @@ var Sandbox = class _Sandbox {
9565
9622
  );
9566
9623
  }
9567
9624
  /**
9568
- * Block until the sandbox reports ready, or *timeout* seconds elapse.
9625
+ * Block until the sandbox reports ready, or the caller's *timeout* seconds
9626
+ * (the full deadline budget) elapse.
9569
9627
  *
9570
- * When `stream` is `true` (the default) this opens an SSE connection
9571
- * to `GET /sandboxes/:id/readiness/stream` and waits for an
9572
- * `event: ready` frame. The server emits `ready` immediately if the
9573
- * sandbox is already ready, otherwise as soon as the readiness PubSub
9574
- * message fires.
9628
+ * When `stream` is `true` (the default) this opens an SSE connection to
9629
+ * `GET /sandboxes/:id/readiness/stream?timeout=<window>` and waits for an
9630
+ * `event: ready` frame. `<window>` is the caller's remaining budget clamped
9631
+ * to {@link READINESS_STREAM_MAX_TIMEOUT_SEC}, the server's own maximum
9632
+ * lease for a single stream connection — a long-restoring persistent
9633
+ * sandbox (e.g. Docker restore, 60-90 s) legitimately outlives one lease.
9634
+ * The server emits `ready` immediately if the sandbox is already ready,
9635
+ * `event: timeout` when *that connection's* window elapses without a
9636
+ * verdict, and `event: error` on a terminal boot failure.
9575
9637
  *
9576
- * Returns `true` once the sandbox is ready, `false` on `event: timeout`
9577
- * or when the local timeout elapses before ready.
9638
+ * An `event: timeout` (or the stream closing without a verdict) is NOT
9639
+ * treated as the final answer while the caller's overall deadline still has
9640
+ * budget left — that would silently truncate a 180 s wait down to the
9641
+ * server's lease window and return `false` while the sandbox was still
9642
+ * mid-restore. Instead this reconnects the stream (or falls back to
9643
+ * polling) for the remaining budget. `false` is only returned once the
9644
+ * caller's full deadline has elapsed.
9578
9645
  *
9579
9646
  * Throws {@link MiosaError} with code `SANDBOX_BOOT_FAILED` as soon as the
9580
9647
  * sandbox is observed in a terminal failure state (`error` or `destroyed`)
9581
9648
  * while waiting, instead of silently exhausting the timeout and returning
9582
9649
  * `false` — a permanent boot failure and "still booting, keep waiting" must
9583
- * never look the same to the caller. The readiness SSE stream itself has no
9584
- * dedicated failure event today (the server only ever emits
9585
- * `ready`/`status`/`timeout`), so on `event: timeout` this makes one
9586
- * authoritative {@link readiness} check before giving up, to catch a
9587
- * failure that landed right at the boundary.
9650
+ * never look the same to the caller. On each `event: timeout` boundary this
9651
+ * also makes one authoritative {@link readiness} check before deciding to
9652
+ * reconnect or stop, to catch a failure that landed right at the boundary.
9588
9653
  *
9589
9654
  * If the SSE endpoint returns 404 (server pre-dates the streaming
9590
9655
  * endpoint) this transparently falls back to polling {@link readiness}
9591
- * every 10 ms until ready, timeout, or terminal failure.
9656
+ * every 10 ms until ready, the caller's deadline elapses, or terminal
9657
+ * failure.
9592
9658
  */
9593
9659
  async waitUntilReady(options = {}) {
9594
9660
  const timeout = options.timeout ?? 30;
9595
9661
  const stream = options.stream ?? true;
9596
9662
  const requireCommandReady = options.requireCommandReady ?? true;
9663
+ const deadlineMs = Date.now() + timeout * 1e3;
9597
9664
  if (stream) {
9598
- const sseResult = await this.tryReadinessStream(timeout);
9599
- if (sseResult === true) {
9600
- await this.adoptReadyState();
9601
- return true;
9602
- }
9603
- if (sseResult === false) {
9604
- await this.throwIfTerminallyFailed();
9605
- return false;
9665
+ while (true) {
9666
+ const remainingMs = deadlineMs - Date.now();
9667
+ if (remainingMs <= 0) return false;
9668
+ const remainingSec = remainingMs / 1e3;
9669
+ const clamped = remainingSec > READINESS_STREAM_MAX_TIMEOUT_SEC;
9670
+ const windowSec = clamped ? READINESS_STREAM_MAX_TIMEOUT_SEC : remainingSec;
9671
+ const sseResult = await this.tryReadinessStream(windowSec);
9672
+ if (sseResult === true) {
9673
+ await this.adoptReadyState();
9674
+ return true;
9675
+ }
9676
+ if (sseResult === false) {
9677
+ await this.throwIfTerminallyFailed();
9678
+ if (!clamped || Date.now() >= deadlineMs) return false;
9679
+ continue;
9680
+ }
9681
+ break;
9606
9682
  }
9683
+ if (Date.now() >= deadlineMs) return false;
9607
9684
  }
9608
- const deadlineMs = Date.now() + timeout * 1e3;
9609
9685
  while (Date.now() < deadlineMs) {
9610
9686
  try {
9611
9687
  const data = await this.readiness();
@@ -9669,9 +9745,25 @@ var Sandbox = class _Sandbox {
9669
9745
  * stream endpoint is unavailable (404 or transport error) so callers
9670
9746
  * can fall back to polling.
9671
9747
  */
9672
- async tryReadinessStream(timeoutSec) {
9748
+ /**
9749
+ * Opens one readiness SSE connection for at most *windowSec* seconds (this
9750
+ * is a single lease, not the caller's overall {@link waitUntilReady}
9751
+ * budget — see there for how leases are chained). *windowSec* is sent to
9752
+ * the server as `?timeout=<seconds>` so the server's own stream deadline
9753
+ * matches what was actually requested instead of always falling back to
9754
+ * its 30 s default, which is what let a 180 s `waitUntilReady` call get
9755
+ * cut short at 30 s.
9756
+ */
9757
+ async tryReadinessStream(windowSec) {
9758
+ const clampedWindowSec = Math.max(
9759
+ 1,
9760
+ Math.min(windowSec, READINESS_STREAM_MAX_TIMEOUT_SEC)
9761
+ );
9673
9762
  const abort = new AbortController();
9674
- const timer = setTimeout(() => abort.abort(), (timeoutSec + 5) * 1e3);
9763
+ const timer = setTimeout(
9764
+ () => abort.abort(),
9765
+ (clampedWindowSec + 5) * 1e3
9766
+ );
9675
9767
  try {
9676
9768
  const headers = {
9677
9769
  Authorization: `Bearer ${this.http.apiKey}`,
@@ -9683,7 +9775,7 @@ var Sandbox = class _Sandbox {
9683
9775
  "User-Agent": SDK_USER_AGENT
9684
9776
  };
9685
9777
  const response = await fetch(
9686
- `${this.http.baseUrl}/sandboxes/${this.id}/readiness/stream`,
9778
+ `${this.http.baseUrl}/sandboxes/${this.id}/readiness/stream?timeout=${clampedWindowSec}`,
9687
9779
  { method: "GET", headers, signal: abort.signal }
9688
9780
  );
9689
9781
  if (response.status === 404) return null;
@@ -9704,6 +9796,7 @@ var Sandbox = class _Sandbox {
9704
9796
  const evt = line.slice(6).trim();
9705
9797
  if (evt === "ready") return true;
9706
9798
  if (evt === "timeout") return false;
9799
+ if (evt === "error") throw this.bootFailedError("error");
9707
9800
  }
9708
9801
  }
9709
9802
  }
@@ -9714,7 +9807,10 @@ var Sandbox = class _Sandbox {
9714
9807
  }
9715
9808
  }
9716
9809
  return null;
9717
- } catch {
9810
+ } catch (err) {
9811
+ if (err instanceof MiosaError && err.code === "SANDBOX_BOOT_FAILED") {
9812
+ throw err;
9813
+ }
9718
9814
  return null;
9719
9815
  } finally {
9720
9816
  clearTimeout(timer);
@@ -9765,6 +9861,50 @@ var Sandbox = class _Sandbox {
9765
9861
  }
9766
9862
  }
9767
9863
  };
9864
+ function sleep8(ms) {
9865
+ return new Promise((resolve) => setTimeout(resolve, ms));
9866
+ }
9867
+ function isResumableState(state) {
9868
+ return state === "paused" || state === "stopped";
9869
+ }
9870
+ var DEFAULT_PAUSING_WAIT_TIMEOUT_SEC = 30;
9871
+ var PAUSING_POLL_INTERVAL_MS = 500;
9872
+ var RESUME_RETRY_MAX_ATTEMPTS = 5;
9873
+ var RESUME_RETRY_FALLBACK_DELAY_MS = 500;
9874
+ async function waitOutPausingState(sandbox, timeoutSec) {
9875
+ const deadline = Date.now() + timeoutSec * 1e3;
9876
+ while (sandbox.state === "pausing" && Date.now() < deadline) {
9877
+ await sleep8(PAUSING_POLL_INTERVAL_MS);
9878
+ try {
9879
+ await sandbox.refresh();
9880
+ } catch {
9881
+ }
9882
+ }
9883
+ }
9884
+ async function resumeWithRetryHint(sandbox) {
9885
+ for (let attempt = 0; ; attempt++) {
9886
+ try {
9887
+ await sandbox.resume();
9888
+ return;
9889
+ } catch (error) {
9890
+ const canRetry = error instanceof MiosaError && error.status === 409 && error.retryable === true && attempt < RESUME_RETRY_MAX_ATTEMPTS;
9891
+ if (!canRetry) throw error;
9892
+ await sleep8(error.retryAfterMs ?? RESUME_RETRY_FALLBACK_DELAY_MS);
9893
+ }
9894
+ }
9895
+ }
9896
+ async function resumeForGetOrCreate(sandbox, params) {
9897
+ if (params.resume === false) return;
9898
+ if (sandbox.state === "pausing") {
9899
+ await waitOutPausingState(
9900
+ sandbox,
9901
+ params.pausingWaitTimeoutSec ?? DEFAULT_PAUSING_WAIT_TIMEOUT_SEC
9902
+ );
9903
+ }
9904
+ if (isResumableState(sandbox.state)) {
9905
+ await resumeWithRetryHint(sandbox);
9906
+ }
9907
+ }
9768
9908
  var Sandboxes = class {
9769
9909
  constructor(http) {
9770
9910
  this.http = http;
@@ -9860,9 +10000,14 @@ var Sandboxes = class {
9860
10000
  /**
9861
10001
  * Idempotently obtain a persistent sandbox by stable name.
9862
10002
  *
9863
- * If the sandbox exists, it is returned. If it is paused and `resume !== false`,
9864
- * it is resumed first. If it does not exist, it is created with the supplied
9865
- * params. Destroyed sandboxes are permanent and are not resumed by this helper.
10003
+ * If the sandbox exists, it is returned. Unless `resume === false`: a
10004
+ * `paused` or persistent `stopped` sandbox is resumed; a `pausing`
10005
+ * sandbox is waited out (bounded by `pausingWaitTimeoutSec`) and then
10006
+ * resumed if it landed on `paused`/`stopped`; a resume answered with a
10007
+ * 409 the server marks `retryable: true` (e.g. a concurrent pause still
10008
+ * finishing) is retried using the server's `retry_after_ms` hint. If the
10009
+ * sandbox does not exist, it is created with the supplied params.
10010
+ * Destroyed sandboxes are permanent and are not resumed by this helper.
9866
10011
  */
9867
10012
  async getOrCreate(params) {
9868
10013
  let sandbox;
@@ -9872,9 +10017,7 @@ var Sandboxes = class {
9872
10017
  if (!(error instanceof NotFoundError)) throw error;
9873
10018
  sandbox = await this.create(params);
9874
10019
  }
9875
- if (sandbox.state === "paused" && params.resume !== false) {
9876
- await sandbox.resume();
9877
- }
10020
+ await resumeForGetOrCreate(sandbox, params);
9878
10021
  if (params.waitUntilReady === true) {
9879
10022
  const ready = await sandbox.waitUntilReady({
9880
10023
  timeout: params.waitTimeoutSec ?? 60