@miosa/sdk 3.2.4 → 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.d.ts CHANGED
@@ -5219,6 +5219,8 @@ interface MiosaErrorBody {
5219
5219
  message?: string;
5220
5220
  details?: unknown;
5221
5221
  retryable?: boolean;
5222
+ request_id?: string;
5223
+ retry_after_ms?: number;
5222
5224
  };
5223
5225
  message?: string;
5224
5226
  code?: string;
@@ -5227,6 +5229,7 @@ interface MiosaErrorBody {
5227
5229
  reason?: string;
5228
5230
  request_id?: string;
5229
5231
  retryable?: boolean;
5232
+ retry_after_ms?: number;
5230
5233
  }
5231
5234
  declare class MiosaError extends Error {
5232
5235
  readonly status: number;
@@ -5234,8 +5237,21 @@ declare class MiosaError extends Error {
5234
5237
  readonly details: unknown;
5235
5238
  readonly requestId: string | undefined;
5236
5239
  readonly retryable: boolean;
5237
- constructor(message: string, status: number, code: string, details?: unknown, requestId?: string, retryable?: boolean);
5238
- static fromResponse(status: number, body: MiosaErrorBody, requestId?: string): MiosaError;
5240
+ /**
5241
+ * Server-supplied retry hint in milliseconds, taken from the canonical
5242
+ * error shape's `retry_after_ms` (nested under `error` or top-level) or,
5243
+ * failing that, the HTTP `Retry-After` header. `undefined` when the
5244
+ * response carried neither, in which case a caller should fall back to
5245
+ * its own backoff policy.
5246
+ */
5247
+ retryAfterMs: number | undefined;
5248
+ constructor(message: string, status: number, code: string, details?: unknown, requestId?: string, retryable?: boolean, retryAfterMs?: number);
5249
+ /**
5250
+ * @param retryAfterHeaderMs The `Retry-After` header value, already
5251
+ * converted to milliseconds by the caller. Used only when the body does
5252
+ * not carry the more precise `retry_after_ms`.
5253
+ */
5254
+ static fromResponse(status: number, body: MiosaErrorBody, requestId?: string, retryAfterHeaderMs?: number): MiosaError;
5239
5255
  }
5240
5256
  declare class AuthError extends MiosaError {
5241
5257
  constructor(message: string, status?: number, code?: string, details?: unknown, requestId?: string, retryable?: boolean);
@@ -6732,7 +6748,7 @@ type SandboxSize = "xs" | "small" | "medium" | "large" | "xl";
6732
6748
  type SandboxId = string & {
6733
6749
  readonly __brand: "SandboxId";
6734
6750
  };
6735
- type SandboxState = "provisioning" | "running" | "paused" | "destroying" | "destroyed" | "error";
6751
+ type SandboxState = "provisioning" | "running" | "pausing" | "paused" | "resuming" | "stopped" | "destroying" | "destroyed" | "error";
6736
6752
  interface SandboxCreateParams {
6737
6753
  templateId?: string;
6738
6754
  template_id?: string;
@@ -6827,8 +6843,18 @@ interface SandboxCreateParams {
6827
6843
  }
6828
6844
  interface SandboxGetOrCreateParams extends SandboxCreateParams {
6829
6845
  name: string;
6830
- /** Resume an existing paused sandbox before returning it. Defaults to true. */
6846
+ /**
6847
+ * Resume an existing sandbox before returning it, when it is `paused`,
6848
+ * `stopped` (persistent sandboxes only), or - after a bounded wait - lands
6849
+ * in one of those states from `pausing`. Defaults to true.
6850
+ */
6831
6851
  resume?: boolean;
6852
+ /**
6853
+ * Bound on how long to wait out an in-progress `pausing` transition before
6854
+ * attempting resume, in seconds. The server's own pause work is typically
6855
+ * well under this. Defaults to 30.
6856
+ */
6857
+ pausingWaitTimeoutSec?: number;
6832
6858
  /**
6833
6859
  * Wait for readiness before returning a created/resumed sandbox. Defaults to false.
6834
6860
  *
@@ -6861,6 +6887,18 @@ interface SandboxExecOptions {
6861
6887
  timeout?: number;
6862
6888
  timeoutSec?: number;
6863
6889
  timeout_sec?: number;
6890
+ /**
6891
+ * `exec.stream` only. Budget (seconds) reserved for the sandbox to wake
6892
+ * and resume before the command's own `timeoutSec` starts counting down,
6893
+ * separate from it. The server opens the SSE response, runs
6894
+ * `ensure_running` (which resumes a paused sandbox), and only then starts
6895
+ * the command's own timeout - so without a separate budget a slow resume
6896
+ * could abort a command client-side while it is still within its own
6897
+ * server-side deadline. Defaults to {@link DEFAULT_EXEC_WAKE_BUDGET_SEC}.
6898
+ * Has no effect when `timeoutSec` is omitted (the stream is unbounded).
6899
+ */
6900
+ wakeTimeoutSec?: number;
6901
+ wake_timeout_sec?: number;
6864
6902
  }
6865
6903
  interface SandboxExecResult {
6866
6904
  stdout: string;
@@ -6869,6 +6907,14 @@ interface SandboxExecResult {
6869
6907
  exit_code: number;
6870
6908
  durationMs?: number;
6871
6909
  duration_ms?: number;
6910
+ /**
6911
+ * True when the server killed the command for exceeding its timeout.
6912
+ * Without this, a timeout was indistinguishable from the infra error
6913
+ * frame, which also reports `exitCode: -1`. Absent when the server did
6914
+ * not report it (older servers, or a non-timeout exit).
6915
+ */
6916
+ timedOut?: boolean;
6917
+ timed_out?: boolean;
6872
6918
  }
6873
6919
  interface SandboxExportFile {
6874
6920
  path: string;
@@ -6902,7 +6948,10 @@ interface SandboxExportParams {
6902
6948
  * The server tags each SSE frame with an `event:` name (`stdout` / `stderr` /
6903
6949
  * `exit`); the SDK maps that name onto `type`. For output frames `data` (and
6904
6950
  * its legacy alias `line`) carry the text chunk. The exit frame carries the
6905
- * process exit code as both `exit_code` and its camelCase alias `exitCode`.
6951
+ * process exit code as both `exit_code` and its camelCase alias `exitCode`,
6952
+ * plus `timed_out`/`timedOut` when the server reports it (see B1-9: without
6953
+ * this a timeout was indistinguishable from the infra error frame, which
6954
+ * also reports `exit_code: -1`).
6906
6955
  */
6907
6956
  type SandboxExecEvent = {
6908
6957
  type: "stdout";
@@ -6916,6 +6965,8 @@ type SandboxExecEvent = {
6916
6965
  type: "exit";
6917
6966
  exit_code: number;
6918
6967
  exitCode: number;
6968
+ timed_out?: boolean;
6969
+ timedOut?: boolean;
6919
6970
  };
6920
6971
  interface SandboxExecRunner {
6921
6972
  (command: string, options?: SandboxExecOptions): Promise<SandboxExecResult>;
@@ -7457,6 +7508,17 @@ declare class Sandbox {
7457
7508
  [key: string]: unknown;
7458
7509
  }>;
7459
7510
  pause(): Promise<Sandbox>;
7511
+ /**
7512
+ * Resume a paused (or persistent-stopped) sandbox.
7513
+ *
7514
+ * Always sends an `Idempotency-Key`, generating a fresh one when the
7515
+ * caller doesn't supply one - same rationale as {@link generateIdempotencyKey}
7516
+ * for create(): the key is built once here and threaded through a single
7517
+ * `HttpClient.request` call, whose headers are fixed before its retry loop
7518
+ * runs, so every retry of *this* resume replays the same key. Without this,
7519
+ * a client-side retry of a slow resume could dispatch a second full resume
7520
+ * execution (re-running host reservation) instead of deduping server-side.
7521
+ */
7460
7522
  resume(idempotencyKey?: string): Promise<Sandbox>;
7461
7523
  deploy(params?: SandboxDeployParams): Promise<Record<string, unknown>>;
7462
7524
  deployDocker(params?: SandboxDeployParams): Promise<Record<string, unknown>>;
@@ -7575,9 +7637,14 @@ declare class Sandboxes {
7575
7637
  /**
7576
7638
  * Idempotently obtain a persistent sandbox by stable name.
7577
7639
  *
7578
- * If the sandbox exists, it is returned. If it is paused and `resume !== false`,
7579
- * it is resumed first. If it does not exist, it is created with the supplied
7580
- * params. Destroyed sandboxes are permanent and are not resumed by this helper.
7640
+ * If the sandbox exists, it is returned. Unless `resume === false`: a
7641
+ * `paused` or persistent `stopped` sandbox is resumed; a `pausing`
7642
+ * sandbox is waited out (bounded by `pausingWaitTimeoutSec`) and then
7643
+ * resumed if it landed on `paused`/`stopped`; a resume answered with a
7644
+ * 409 the server marks `retryable: true` (e.g. a concurrent pause still
7645
+ * finishing) is retried using the server's `retry_after_ms` hint. If the
7646
+ * sandbox does not exist, it is created with the supplied params.
7647
+ * Destroyed sandboxes are permanent and are not resumed by this helper.
7581
7648
  */
7582
7649
  getOrCreate(params: SandboxGetOrCreateParams): Promise<Sandbox>;
7583
7650
  delete(id: SandboxId | string): Promise<void>;
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.4";
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) {
@@ -8652,16 +8666,29 @@ function raiseUnresolvableShape(cpuCount, memoryMb, tier) {
8652
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.`
8653
8667
  );
8654
8668
  }
8655
- function execDeadline(options) {
8669
+ function validatedExecSeconds(options) {
8656
8670
  const seconds = options?.timeout ?? options?.timeoutSec ?? options?.timeout_sec;
8657
- if (seconds === void 0) return {};
8671
+ if (seconds === void 0) return void 0;
8658
8672
  if (!Number.isFinite(seconds) || seconds <= 0 || seconds > 86400) {
8659
8673
  throw new RangeError(
8660
8674
  "Exec timeout must be greater than zero and at most 86400 seconds"
8661
8675
  );
8662
8676
  }
8677
+ return seconds;
8678
+ }
8679
+ function execDeadline(options) {
8680
+ const seconds = validatedExecSeconds(options);
8681
+ if (seconds === void 0) return {};
8663
8682
  return { timeout: seconds * 1e3 + 5e3 };
8664
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
+ }
8665
8692
  function execBody(command, options = {}) {
8666
8693
  return stripUndefined26({
8667
8694
  command,
@@ -8677,7 +8704,14 @@ function normalizeExecEvent(event, payload) {
8677
8704
  const code = Number(
8678
8705
  record.exit_code ?? record.exitCode ?? (typeof payload === "number" ? payload : 0)
8679
8706
  );
8680
- 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
+ };
8681
8715
  }
8682
8716
  const type = event === "stderr" ? "stderr" : "stdout";
8683
8717
  const data = typeof payload === "string" ? payload : String(record.line ?? record.data ?? "");
@@ -9145,12 +9179,18 @@ var Sandbox = class _Sandbox {
9145
9179
  result.durationMs = response.duration_ms;
9146
9180
  result.duration_ms = response.duration_ms;
9147
9181
  }
9182
+ const timedOutRaw = response.timed_out ?? response.timedOut;
9183
+ if (typeof timedOutRaw === "boolean") {
9184
+ result.timedOut = timedOutRaw;
9185
+ result.timed_out = timedOutRaw;
9186
+ }
9148
9187
  return result;
9149
9188
  }
9150
9189
  async runCancellableExec(command, options) {
9151
9190
  let stdout = "";
9152
9191
  let stderr = "";
9153
9192
  let exitCode;
9193
+ let timedOut;
9154
9194
  for await (const event of this.execStream(command, options)) {
9155
9195
  if (event.type === "stdout" && typeof event.line === "string")
9156
9196
  stdout += event.line;
@@ -9158,6 +9198,7 @@ var Sandbox = class _Sandbox {
9158
9198
  stderr += event.line;
9159
9199
  if (event.type === "exit" && typeof event.exit_code === "number") {
9160
9200
  exitCode = event.exit_code;
9201
+ timedOut = event.timed_out;
9161
9202
  break;
9162
9203
  }
9163
9204
  }
@@ -9172,7 +9213,13 @@ var Sandbox = class _Sandbox {
9172
9213
  false
9173
9214
  );
9174
9215
  }
9175
- 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
+ };
9176
9223
  }
9177
9224
  execStream(command, options) {
9178
9225
  this.assertRunning("exec.stream");
@@ -9182,7 +9229,7 @@ var Sandbox = class _Sandbox {
9182
9229
  method: "POST",
9183
9230
  body: execBody(command, options),
9184
9231
  signal: options?.signal,
9185
- ...execDeadline(options)
9232
+ ...execStreamDeadline(options)
9186
9233
  }
9187
9234
  );
9188
9235
  return (async function* () {
@@ -9456,17 +9503,26 @@ var Sandbox = class _Sandbox {
9456
9503
  this.data = { ...this.data, ...data };
9457
9504
  return this;
9458
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
+ */
9459
9517
  async resume(idempotencyKey11) {
9460
- const response = idempotencyKey11 ? await this.http.request(
9518
+ const key = idempotencyKey11 ?? generateIdempotencyKey();
9519
+ const response = await this.http.request(
9461
9520
  `/sandboxes/${this.id}/resume`,
9462
9521
  {
9463
9522
  method: "POST",
9464
9523
  body: {},
9465
- headers: { "Idempotency-Key": idempotencyKey11 }
9524
+ headers: { "Idempotency-Key": key }
9466
9525
  }
9467
- ) : await this.http.post(
9468
- `/sandboxes/${this.id}/resume`,
9469
- {}
9470
9526
  );
9471
9527
  const data = unwrap48(response);
9472
9528
  this.data = { ...this.data, ...data };
@@ -9805,6 +9861,50 @@ var Sandbox = class _Sandbox {
9805
9861
  }
9806
9862
  }
9807
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
+ }
9808
9908
  var Sandboxes = class {
9809
9909
  constructor(http) {
9810
9910
  this.http = http;
@@ -9900,9 +10000,14 @@ var Sandboxes = class {
9900
10000
  /**
9901
10001
  * Idempotently obtain a persistent sandbox by stable name.
9902
10002
  *
9903
- * If the sandbox exists, it is returned. If it is paused and `resume !== false`,
9904
- * it is resumed first. If it does not exist, it is created with the supplied
9905
- * 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.
9906
10011
  */
9907
10012
  async getOrCreate(params) {
9908
10013
  let sandbox;
@@ -9912,9 +10017,7 @@ var Sandboxes = class {
9912
10017
  if (!(error instanceof NotFoundError)) throw error;
9913
10018
  sandbox = await this.create(params);
9914
10019
  }
9915
- if (sandbox.state === "paused" && params.resume !== false) {
9916
- await sandbox.resume();
9917
- }
10020
+ await resumeForGetOrCreate(sandbox, params);
9918
10021
  if (params.waitUntilReady === true) {
9919
10022
  const ready = await sandbox.waitUntilReady({
9920
10023
  timeout: params.waitTimeoutSec ?? 60