@miosa/sdk 3.2.2 → 3.2.3

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/README.md CHANGED
@@ -268,6 +268,23 @@ await sbx.pause();
268
268
  await sbx.resume();
269
269
  ```
270
270
 
271
+ ### Cancelling sandbox commands
272
+
273
+ Pass an `AbortSignal` to `sandbox.exec.run(command, { signal, timeoutSec })` or `sandbox.exec.stream(command, { signal, timeoutSec })`.
274
+ Aborting rejects with the signal's abort reason (an `AbortError` by default); it does not return a successful exit code or replay the command.
275
+ Signal-bearing `exec.run` uses the request-owned stream and aggregates its exact stdout, stderr, and terminal exit code.
276
+ If that stream ends without an exit event, `exec.run` rejects with `EXEC_STREAM_INCOMPLETE` (502, not retryable).
277
+ Ordinary execution without a signal keeps the standard exec endpoint.
278
+ An explicit `timeoutSec` sets the guest deadline and allows five additional seconds for its response.
279
+ A guest that overruns that deadline rejects with `TimeoutError`, never with `AbortError`.
280
+ Cancellation requires the server's request-owned streaming implementation and the guest's process-group cancellation support.
281
+
282
+ ### Confirming sandbox deletion
283
+
284
+ `sandbox.destroy()` acknowledges deletion and updates the handle to the server's state.
285
+ If deletion is still in progress, the handle reports `destroying` and exposes `sandbox.data.operation_id` when supplied by the server.
286
+ Use `sandbox.refresh()` to verify completion; an acknowledgement alone does not prove resource cleanup.
287
+
271
288
  ## Publish from sandbox
272
289
 
273
290
  Preview is mutable; publish creates a durable deployment version. Static output
@@ -414,6 +431,7 @@ try {
414
431
  ```
415
432
 
416
433
  The SDK retries `429` and `5xx` automatically (3 retries, exponential backoff + jitter). Set `maxRetries: 0` to disable.
434
+ Sandbox `exec` requests are never retried, because a replayed command would run twice; see [Cancelling sandbox commands](#cancelling-sandbox-commands).
417
435
  Every `MiosaError` carries `retryable` (server-supplied when present, otherwise `true` for 408/429/5xx and transport failures) and `requestId` for support correlation.
418
436
 
419
437
  ## Configuration
package/dist/index.d.ts CHANGED
@@ -6,6 +6,10 @@ interface RequestOptions {
6
6
  headers?: Record<string, string>;
7
7
  body?: unknown;
8
8
  timeout?: number;
9
+ /** Cancel this request and its response stream without retrying. */
10
+ signal?: AbortSignal | undefined;
11
+ /** Override retries for operations which cannot safely be replayed. */
12
+ maxRetries?: number;
9
13
  /** Return raw Response instead of parsing JSON */
10
14
  rawResponse?: boolean;
11
15
  /** Send body as multipart/form-data */
@@ -6728,7 +6732,7 @@ type SandboxSize = "xs" | "small" | "medium" | "large" | "xl";
6728
6732
  type SandboxId = string & {
6729
6733
  readonly __brand: "SandboxId";
6730
6734
  };
6731
- type SandboxState = "provisioning" | "running" | "paused" | "destroyed" | "error";
6735
+ type SandboxState = "provisioning" | "running" | "paused" | "destroying" | "destroyed" | "error";
6732
6736
  interface SandboxCreateParams {
6733
6737
  templateId?: string;
6734
6738
  template_id?: string;
@@ -6848,6 +6852,8 @@ interface SandboxListParams {
6848
6852
  external_project_id?: string;
6849
6853
  }
6850
6854
  interface SandboxExecOptions {
6855
+ /** Abort the request-scoped execution, including its response stream. */
6856
+ signal?: AbortSignal;
6851
6857
  cwd?: string;
6852
6858
  workingDir?: string;
6853
6859
  working_dir?: string;
@@ -6917,6 +6923,8 @@ interface SandboxExecRunner {
6917
6923
  stream(command: string, options?: SandboxExecOptions): AsyncIterableIterator<SandboxExecEvent>;
6918
6924
  }
6919
6925
  interface SandboxData {
6926
+ operation_id?: string | null;
6927
+ request_id?: string | null;
6920
6928
  id: SandboxId;
6921
6929
  state: SandboxState;
6922
6930
  slug?: string;
@@ -7372,6 +7380,7 @@ declare class Sandbox {
7372
7380
  */
7373
7381
  prompt(prompt: string, options?: SandboxPromptOptions): Promise<AgentRun>;
7374
7382
  private runExec;
7383
+ private runCancellableExec;
7375
7384
  private execStream;
7376
7385
  writeFile(path: string, content: string | Uint8Array): Promise<void>;
7377
7386
  download(path: string): Promise<Uint8Array>;
package/dist/index.js CHANGED
@@ -174,7 +174,7 @@ var TokenRefreshFailedError = class extends MiosaError {
174
174
  };
175
175
 
176
176
  // src/version.ts
177
- var SDK_VERSION = "3.2.2";
177
+ var SDK_VERSION = "3.2.3";
178
178
  var SDK_USER_AGENT = `@miosa/sdk/${SDK_VERSION}`;
179
179
 
180
180
  // src/http.ts
@@ -271,8 +271,12 @@ var HttpClient = class {
271
271
  fetchBody = JSON.stringify(body5);
272
272
  }
273
273
  let lastError;
274
- for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
274
+ const maxRetries = options.maxRetries ?? this.maxRetries;
275
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
276
+ options.signal?.throwIfAborted();
275
277
  const controller = new AbortController();
278
+ const cancel = () => controller.abort(options.signal?.reason);
279
+ options.signal?.addEventListener("abort", cancel, { once: true });
276
280
  const timer = setTimeout(() => controller.abort(), timeout);
277
281
  try {
278
282
  const response = await fetch(this.buildUrl(path), {
@@ -296,7 +300,7 @@ var HttpClient = class {
296
300
  errorBody,
297
301
  requestId
298
302
  );
299
- if (RETRY_STATUS.has(response.status) && attempt < this.maxRetries) {
303
+ if (RETRY_STATUS.has(response.status) && attempt < maxRetries) {
300
304
  lastError = error;
301
305
  await sleep(retryDelay(attempt, retryAfter));
302
306
  continue;
@@ -313,7 +317,7 @@ var HttpClient = class {
313
317
  if (options.rawResponse) return response;
314
318
  return await response.json();
315
319
  } catch (err) {
316
- clearTimeout(timer);
320
+ options.signal?.throwIfAborted();
317
321
  if (err instanceof MiosaError) {
318
322
  throw err;
319
323
  }
@@ -327,7 +331,7 @@ var HttpClient = class {
327
331
  `Network error reaching ${path}: ${err.message}`,
328
332
  err
329
333
  );
330
- if (attempt < this.maxRetries) {
334
+ if (attempt < maxRetries) {
331
335
  lastError = networkErr;
332
336
  await sleep(retryDelay(attempt, null));
333
337
  continue;
@@ -335,6 +339,9 @@ var HttpClient = class {
335
339
  throw networkErr;
336
340
  }
337
341
  throw err;
342
+ } finally {
343
+ clearTimeout(timer);
344
+ options.signal?.removeEventListener("abort", cancel);
338
345
  }
339
346
  }
340
347
  throw lastError ?? new MiosaError(
@@ -394,44 +401,52 @@ var HttpClient = class {
394
401
  headers = { ...headers, "Content-Type": "application/json" };
395
402
  body5 = JSON.stringify(options.body);
396
403
  }
404
+ options.signal?.throwIfAborted();
397
405
  const controller = new AbortController();
398
- let response;
406
+ const cancel = () => controller.abort(options.signal?.reason);
407
+ options.signal?.addEventListener("abort", cancel, { once: true });
408
+ const deadline = options.timeout === void 0 ? void 0 : new TimeoutError(
409
+ `Stream ${path} timed out after ${options.timeout}ms`
410
+ );
411
+ const timer = deadline === void 0 ? void 0 : setTimeout(() => controller.abort(deadline), options.timeout);
412
+ let reader;
399
413
  try {
400
- response = await fetch(`${this.baseUrl}${path}`, {
401
- method,
402
- headers,
403
- body: body5,
404
- signal: controller.signal
405
- });
406
- } catch (err) {
407
- if (err instanceof TypeError) {
408
- throw new NetworkError(
409
- `Network error opening SSE stream: ${err.message}`,
410
- err
414
+ let response;
415
+ try {
416
+ response = await fetch(this.buildUrl(path), {
417
+ method,
418
+ headers,
419
+ body: body5,
420
+ signal: controller.signal
421
+ });
422
+ } catch (err) {
423
+ if (err instanceof TypeError) {
424
+ throw new NetworkError(
425
+ `Network error opening SSE stream: ${err.message}`,
426
+ err
427
+ );
428
+ }
429
+ throw err;
430
+ }
431
+ if (!response.ok) {
432
+ let errorBody = {};
433
+ try {
434
+ errorBody = await response.json();
435
+ } catch {
436
+ }
437
+ throw MiosaError.fromResponse(
438
+ response.status,
439
+ errorBody,
440
+ response.headers.get("x-request-id") ?? void 0
411
441
  );
412
442
  }
413
- throw err;
414
- }
415
- if (!response.ok) {
416
- let errorBody = {};
417
- try {
418
- errorBody = await response.json();
419
- } catch {
443
+ if (!response.body) {
444
+ throw new MiosaError("SSE stream has no body", 0, "NO_STREAM_BODY");
420
445
  }
421
- throw MiosaError.fromResponse(
422
- response.status,
423
- errorBody,
424
- response.headers.get("x-request-id") ?? void 0
425
- );
426
- }
427
- if (!response.body) {
428
- throw new MiosaError("SSE stream has no body", 0, "NO_STREAM_BODY");
429
- }
430
- const reader = response.body.getReader();
431
- const decoder = new TextDecoder();
432
- let buffer = "";
433
- let event = null;
434
- try {
446
+ reader = response.body.getReader();
447
+ const decoder = new TextDecoder();
448
+ let buffer = "";
449
+ let event = null;
435
450
  while (true) {
436
451
  const { done, value } = await reader.read();
437
452
  if (done) break;
@@ -459,9 +474,17 @@ var HttpClient = class {
459
474
  }
460
475
  }
461
476
  }
477
+ } catch (err) {
478
+ options.signal?.throwIfAborted();
479
+ if (deadline !== void 0 && controller.signal.reason === deadline) {
480
+ throw deadline;
481
+ }
482
+ throw err;
462
483
  } finally {
484
+ clearTimeout(timer);
485
+ options.signal?.removeEventListener("abort", cancel);
463
486
  controller.abort();
464
- reader.releaseLock();
487
+ reader?.releaseLock();
465
488
  }
466
489
  }
467
490
  /**
@@ -8628,6 +8651,16 @@ function raiseUnresolvableShape(cpuCount, memoryMb, tier) {
8628
8651
  `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.`
8629
8652
  );
8630
8653
  }
8654
+ function execDeadline(options) {
8655
+ const seconds = options?.timeout ?? options?.timeoutSec ?? options?.timeout_sec;
8656
+ if (seconds === void 0) return {};
8657
+ if (!Number.isFinite(seconds) || seconds <= 0 || seconds > 86400) {
8658
+ throw new RangeError(
8659
+ "Exec timeout must be greater than zero and at most 86400 seconds"
8660
+ );
8661
+ }
8662
+ return { timeout: seconds * 1e3 + 5e3 };
8663
+ }
8631
8664
  function execBody(command, options = {}) {
8632
8665
  return stripUndefined26({
8633
8666
  command,
@@ -9087,10 +9120,17 @@ var Sandbox = class _Sandbox {
9087
9120
  }
9088
9121
  async runExec(command, options) {
9089
9122
  this.assertRunning("exec");
9123
+ if (options?.signal) return this.runCancellableExec(command, options);
9090
9124
  const response = unwrap48(
9091
- await this.http.post(
9125
+ await this.http.request(
9092
9126
  `/sandboxes/${this.id}/exec`,
9093
- execBody(command, options)
9127
+ {
9128
+ method: "POST",
9129
+ body: execBody(command, options),
9130
+ signal: options?.signal,
9131
+ maxRetries: 0,
9132
+ ...execDeadline(options)
9133
+ }
9094
9134
  )
9095
9135
  );
9096
9136
  const exitCode = Number(response.exit_code ?? response.exitCode ?? 0);
@@ -9106,13 +9146,42 @@ var Sandbox = class _Sandbox {
9106
9146
  }
9107
9147
  return result;
9108
9148
  }
9149
+ async runCancellableExec(command, options) {
9150
+ let stdout = "";
9151
+ let stderr = "";
9152
+ let exitCode;
9153
+ for await (const event of this.execStream(command, options)) {
9154
+ if (event.type === "stdout" && typeof event.line === "string")
9155
+ stdout += event.line;
9156
+ if (event.type === "stderr" && typeof event.line === "string")
9157
+ stderr += event.line;
9158
+ if (event.type === "exit" && typeof event.exit_code === "number") {
9159
+ exitCode = event.exit_code;
9160
+ break;
9161
+ }
9162
+ }
9163
+ options.signal?.throwIfAborted();
9164
+ if (exitCode === void 0) {
9165
+ throw new MiosaError(
9166
+ "Execution stream ended without a terminal result",
9167
+ 502,
9168
+ "EXEC_STREAM_INCOMPLETE",
9169
+ void 0,
9170
+ void 0,
9171
+ false
9172
+ );
9173
+ }
9174
+ return { stdout, stderr, exitCode, exit_code: exitCode };
9175
+ }
9109
9176
  execStream(command, options) {
9110
9177
  this.assertRunning("exec.stream");
9111
9178
  const frames = this.http.streamFrames(
9112
9179
  `/sandboxes/${this.id}/exec/stream`,
9113
9180
  {
9114
9181
  method: "POST",
9115
- body: execBody(command, options)
9182
+ body: execBody(command, options),
9183
+ signal: options?.signal,
9184
+ ...execDeadline(options)
9116
9185
  }
9117
9186
  );
9118
9187
  return (async function* () {
@@ -9656,12 +9725,21 @@ var Sandbox = class _Sandbox {
9656
9725
  }
9657
9726
  }
9658
9727
  async destroy() {
9659
- if (this.data.state === "destroyed") return;
9660
- await this.http.delete(`/sandboxes/${this.id}`);
9728
+ if (this.data.state === "destroyed" || this.data.state === "destroying") {
9729
+ return;
9730
+ }
9731
+ const receipt = unwrap48(
9732
+ await this.http.delete(
9733
+ `/sandboxes/${this.id}`
9734
+ )
9735
+ ) ?? {};
9736
+ const settled = receipt.state === "destroyed" || receipt.state === void 0 && receipt.operation_id == null;
9661
9737
  this.data = {
9662
9738
  ...this.data,
9663
- state: "destroyed",
9664
- destroyed_at: (/* @__PURE__ */ new Date()).toISOString()
9739
+ state: settled ? "destroyed" : "destroying",
9740
+ ready: false,
9741
+ operation_id: receipt.operation_id ?? this.data.operation_id ?? null,
9742
+ destroyed_at: receipt.destroyed_at ?? this.data.destroyed_at ?? null
9665
9743
  };
9666
9744
  }
9667
9745
  delete() {