withruntime 0.11.1 → 0.12.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.
Files changed (176) hide show
  1. package/BLAXEL.md +3 -3
  2. package/COMPATIBILITY.md +74 -9
  3. package/DAYTONA.md +33 -5
  4. package/E2B.md +137 -48
  5. package/README.md +11 -9
  6. package/README.src.md +11 -9
  7. package/VERCEL.md +43 -5
  8. package/compatibility-lock.json +58 -49
  9. package/dist/billing-cli.d.ts +1 -1
  10. package/dist/billing-cli.js +77 -1
  11. package/dist/blaxel/errors.d.ts +14 -0
  12. package/dist/blaxel/errors.js +33 -4
  13. package/dist/blaxel/index.d.ts +2 -2
  14. package/dist/blaxel/index.js +4 -4
  15. package/dist/blaxel/interpreter.d.ts +2 -5
  16. package/dist/blaxel/sandbox.d.ts +15 -7
  17. package/dist/blaxel/sandbox.js +64 -9
  18. package/dist/blaxel/snapshot.d.ts +4 -0
  19. package/dist/blaxel/snapshot.js +4 -0
  20. package/dist/cli/logs.d.ts +21 -0
  21. package/dist/cli/logs.js +188 -0
  22. package/dist/cli/logs.test.d.ts +1 -0
  23. package/dist/cli/logs.test.js +272 -0
  24. package/dist/cli-extras.d.ts +3 -3
  25. package/dist/cli-extras.js +17 -12
  26. package/dist/cli-words.d.ts +10 -1
  27. package/dist/cli-words.js +18 -0
  28. package/dist/cli.d.ts +6 -2
  29. package/dist/cli.js +468 -219
  30. package/dist/client.d.ts +13 -63
  31. package/dist/client.js +35 -124
  32. package/dist/cloudflare/errors.d.ts +143 -0
  33. package/dist/cloudflare/errors.js +167 -0
  34. package/dist/cloudflare/index.d.ts +175 -6
  35. package/dist/cloudflare/index.js +416 -26
  36. package/dist/cloudflare/interpreter.d.ts +119 -0
  37. package/dist/cloudflare/interpreter.js +223 -0
  38. package/dist/cloudflare/streams.d.ts +38 -0
  39. package/dist/cloudflare/streams.js +145 -0
  40. package/dist/cloudflare/watch.d.ts +9 -0
  41. package/dist/cloudflare/watch.js +24 -0
  42. package/dist/codesandbox/index.d.ts +216 -28
  43. package/dist/codesandbox/index.js +478 -78
  44. package/dist/compat/core.d.ts +18 -0
  45. package/dist/compat/core.js +29 -0
  46. package/dist/compat/files.d.ts +1 -0
  47. package/dist/compat/files.js +25 -5
  48. package/dist/daytona/computer-use.d.ts +13 -4
  49. package/dist/daytona/computer-use.js +66 -8
  50. package/dist/daytona/daytona.d.ts +7 -2
  51. package/dist/daytona/daytona.js +31 -5
  52. package/dist/daytona/errors.d.ts +11 -0
  53. package/dist/daytona/errors.js +18 -0
  54. package/dist/daytona/filesystem.js +2 -1
  55. package/dist/daytona/image.d.ts +13 -9
  56. package/dist/daytona/image.js +18 -12
  57. package/dist/daytona/index.d.ts +1 -1
  58. package/dist/daytona/sandbox.d.ts +9 -3
  59. package/dist/daytona/sandbox.js +17 -3
  60. package/dist/domains/index.d.ts +9 -0
  61. package/dist/domains/index.js +10 -0
  62. package/dist/e2b/client.d.ts +15 -2
  63. package/dist/e2b/client.js +61 -7
  64. package/dist/e2b/commands.d.ts +4 -1
  65. package/dist/e2b/commands.js +17 -7
  66. package/dist/e2b/errors.d.ts +43 -2
  67. package/dist/e2b/errors.js +78 -6
  68. package/dist/e2b/filesystem.d.ts +1 -1
  69. package/dist/e2b/filesystem.js +82 -16
  70. package/dist/e2b/index.d.ts +3 -3
  71. package/dist/e2b/index.js +2 -2
  72. package/dist/e2b/sandbox.d.ts +50 -35
  73. package/dist/e2b/sandbox.js +228 -185
  74. package/dist/e2b/unsupported.d.ts +19 -0
  75. package/dist/e2b/unsupported.js +22 -1
  76. package/dist/e2b/users.d.ts +9 -0
  77. package/dist/e2b/users.js +22 -0
  78. package/dist/errors.d.ts +14 -5
  79. package/dist/errors.js +28 -14
  80. package/dist/freestyle/index.d.ts +285 -37
  81. package/dist/freestyle/index.js +656 -144
  82. package/dist/identity.d.ts +3 -0
  83. package/dist/identity.js +27 -0
  84. package/dist/images/index.d.ts +9 -0
  85. package/dist/images/index.js +10 -0
  86. package/dist/index.d.ts +1 -0
  87. package/dist/ips/index.d.ts +9 -0
  88. package/dist/ips/index.js +10 -0
  89. package/dist/jobs/index.d.ts +9 -0
  90. package/dist/jobs/index.js +10 -0
  91. package/dist/keys.js +2 -0
  92. package/dist/login.js +2 -0
  93. package/dist/machine.d.ts +189 -0
  94. package/dist/machine.js +521 -0
  95. package/dist/mcp.js +1 -1
  96. package/dist/modal/index.d.ts +339 -61
  97. package/dist/modal/index.js +781 -121
  98. package/dist/network-cli.d.ts +3 -1
  99. package/dist/network-cli.js +78 -36
  100. package/dist/networks/index.d.ts +11 -0
  101. package/dist/networks/index.js +10 -0
  102. package/dist/openai-agents/index.d.ts +1 -1
  103. package/dist/openai-agents/index.js +1 -1
  104. package/dist/ports/index.d.ts +9 -0
  105. package/dist/ports/index.js +10 -0
  106. package/dist/product-names.d.ts +52 -0
  107. package/dist/product-names.js +164 -0
  108. package/dist/product-renames.d.ts +103 -0
  109. package/dist/product-renames.js +189 -0
  110. package/dist/products/apps.d.ts +176 -0
  111. package/dist/products/apps.js +165 -0
  112. package/dist/products/billing.d.ts +41 -0
  113. package/dist/products/billing.js +13 -0
  114. package/dist/products/browser.d.ts +10 -0
  115. package/dist/products/browsers.d.ts +71 -0
  116. package/dist/products/browsers.js +102 -0
  117. package/dist/products/builds.d.ts +126 -0
  118. package/dist/products/builds.js +221 -0
  119. package/dist/products/index.d.ts +3 -16
  120. package/dist/products/index.js +3 -14
  121. package/dist/products/jobs.d.ts +19 -3
  122. package/dist/products/jobs.js +28 -12
  123. package/dist/products/limits.d.ts +4 -10
  124. package/dist/products/logs.d.ts +245 -0
  125. package/dist/products/logs.js +274 -0
  126. package/dist/products/logs.test.d.ts +1 -0
  127. package/dist/products/logs.test.js +343 -0
  128. package/dist/products/network-products.d.ts +38 -7
  129. package/dist/products/network-products.js +31 -18
  130. package/dist/products/network.d.ts +1 -1
  131. package/dist/products/observability.d.ts +3 -2
  132. package/dist/products/private-network.js +1 -1
  133. package/dist/products/queues.d.ts +149 -0
  134. package/dist/products/queues.js +213 -0
  135. package/dist/products/referrals.d.ts +21 -7
  136. package/dist/products/referrals.js +3 -3
  137. package/dist/products/secrets.d.ts +8 -4
  138. package/dist/products/secrets.js +39 -18
  139. package/dist/products/signed-requests.d.ts +21 -0
  140. package/dist/products/signed-requests.js +11 -0
  141. package/dist/products/streams.d.ts +73 -0
  142. package/dist/products/streams.js +17 -0
  143. package/dist/products/switching.d.ts +37 -10
  144. package/dist/products/switching.js +4 -3
  145. package/dist/products/volumes.d.ts +3 -0
  146. package/dist/record-cli.d.ts +23 -0
  147. package/dist/record-cli.js +308 -0
  148. package/dist/renamed-products.d.ts +38 -0
  149. package/dist/renamed-products.js +34 -0
  150. package/dist/runloop/index.d.ts +9 -2
  151. package/dist/runloop/index.js +29 -8
  152. package/dist/runloop/sdk.d.ts +131 -0
  153. package/dist/runloop/sdk.js +167 -0
  154. package/dist/sandbox.d.ts +10 -94
  155. package/dist/sandbox.js +22 -371
  156. package/dist/sandboxes/index.d.ts +11 -0
  157. package/dist/sandboxes/index.js +12 -0
  158. package/dist/snapshots/index.d.ts +9 -0
  159. package/dist/snapshots/index.js +11 -0
  160. package/dist/snapshots.d.ts +7 -4
  161. package/dist/sprites/index.d.ts +343 -31
  162. package/dist/sprites/index.js +866 -84
  163. package/dist/ssh.d.ts +1 -1
  164. package/dist/ssh.js +11 -11
  165. package/dist/terminal.d.ts +24 -0
  166. package/dist/terminal.js +32 -0
  167. package/dist/transport.d.ts +18 -2
  168. package/dist/transport.js +61 -6
  169. package/dist/tunnel.js +1 -1
  170. package/dist/types.d.ts +116 -8
  171. package/dist/vercel/sandbox.d.ts +1 -2
  172. package/dist/vercel/sandbox.js +6 -5
  173. package/dist/vercel/snapshot.js +10 -2
  174. package/dist/volumes/index.d.ts +9 -0
  175. package/dist/volumes/index.js +10 -0
  176. package/package.json +48 -2
package/dist/sandbox.js CHANGED
@@ -1,13 +1,13 @@
1
- import { KEEP_ALIVE_MARGIN_SECONDS, STREAMED_EXEC_TIMEOUT_MS, WAIT_FOR_TIMEOUT_SECONDS, } from "./api-defaults.js";
1
+ import { STREAMED_EXEC_TIMEOUT_MS } from "./api-defaults.js";
2
2
  import { CommandError, ConnectionError, DELIBERATE, errorFor, NotFoundError, RateLimitError, RuntimeError, ServiceUnavailableError, } from "./errors.js";
3
- import { Page } from "./page.js";
4
3
  import { sandboxWatches, watchDirectory, } from "./products/watch.js";
5
- import { Transport } from "./transport.js";
4
+ import { Transport, timeoutSignal } from "./transport.js";
6
5
  import { sandboxFactories } from "./products/index.js";
7
6
  import { Tunnel } from "./tunnel.js";
8
7
  import { pause } from "./wait.js";
9
- /** How long snapshot() waits, pause and capture together, unless told. */
10
- const SNAPSHOT_DEADLINE_MS = 10 * 60_000;
8
+ import { Machine, machinePage, requestOnly } from "./machine.js";
9
+ /** Sandboxes, as a product of the machine engine (machine.ts). */
10
+ export const SANDBOXES = { plural: "sandboxes", noun: "Sandbox" };
11
11
  const CHUNK = 1_048_576;
12
12
  /** Chunks of a large write in flight at once. Each chunk's reply waits on the
13
13
  * API and the guest, and the link idles while every chunk in flight waits:
@@ -20,7 +20,7 @@ const enc = (id) => encodeURIComponent(id);
20
20
  function requestScope(options) {
21
21
  const deadline = options.timeoutMs === undefined || options.timeoutMs === 0
22
22
  ? undefined
23
- : AbortSignal.timeout(options.timeoutMs);
23
+ : timeoutSignal(options.timeoutMs);
24
24
  const signal = deadline && options.signal
25
25
  ? AbortSignal.any([deadline, options.signal])
26
26
  : (deadline ?? options.signal);
@@ -59,15 +59,6 @@ function requestScope(options) {
59
59
  };
60
60
  return { request, check, wait };
61
61
  }
62
- /** DELETE /v1/sandboxes/{id}, for `sandbox.delete()` and
63
- * `runtime.sandboxes.delete(id)`. */
64
- export function deleteSandbox(t, id, options = {}) {
65
- return t.json({
66
- method: "DELETE",
67
- path: `/v1/sandboxes/${enc(id)}`,
68
- ...options,
69
- });
70
- }
71
62
  /* Web APIs only, so the sandbox's commands, processes and files work in a
72
63
  browser with a session token (Sandbox.fromSession): no Buffer, no
73
64
  node:crypto. */
@@ -97,26 +88,21 @@ function commandBody(command, options) {
97
88
  };
98
89
  }
99
90
  // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
100
- export class Sandbox {
101
- #info;
102
- #keepAlive;
91
+ export class Sandbox extends Machine {
103
92
  #t;
104
93
  files;
105
94
  processes;
106
95
  /** Short-lived tokens that let a browser reach this sandbox directly. */
107
96
  sessions;
108
97
  constructor(transport, info) {
98
+ super(SANDBOXES, transport, info);
109
99
  this.#t = transport;
110
- this.#info = info;
111
100
  this.files = new Files(transport, info.id);
112
101
  this.processes = new Processes(transport, info.id);
113
102
  this.sessions = new SandboxSessions(transport, info.id);
114
103
  for (const [name, make] of sandboxFactories())
115
104
  Object.defineProperty(this, name, { value: make(transport, this), enumerable: false });
116
105
  }
117
- get id() {
118
- return this.#info.id;
119
- }
120
106
  /** A sandbox reached with a session token instead of an API key, for code in
121
107
  * a browser: commands and their streams, processes, files and previews of
122
108
  * this one sandbox. Makes no call; `info` holds only the id until
@@ -141,41 +127,6 @@ export class Sandbox {
141
127
  });
142
128
  return new Sandbox(transport, { id: session.sandboxId });
143
129
  }
144
- /** The API's latest answer. `start`, the report of an image's start
145
- * command, is the create's alone: the API keeps no record of it, so a
146
- * later read of this sandbox keeps it here. */
147
- #keep(info) {
148
- const start = this.#info?.start;
149
- this.#info = info.start === undefined && start !== undefined ? { ...info, start } : info;
150
- }
151
- /** What the API last said about this sandbox. `refresh()` asks again. */
152
- get info() {
153
- return this.#info;
154
- }
155
- get state() {
156
- return this.#info.state;
157
- }
158
- async refresh(options = {}) {
159
- this.#keep(await this.#t.json({
160
- method: "GET",
161
- path: `/v1/sandboxes/${enc(this.id)}`,
162
- ...options,
163
- }));
164
- return this;
165
- }
166
- /** Waits (server-side, no polling) until the sandbox reaches `state`. */
167
- async waitFor(state, options = {}) {
168
- this.#keep(await this.#t.json({
169
- method: "GET",
170
- path: `/v1/sandboxes/${enc(this.id)}`,
171
- query: {
172
- waitFor: state,
173
- timeoutSeconds: options.timeoutSeconds ?? WAIT_FOR_TIMEOUT_SECONDS,
174
- },
175
- ...options,
176
- }));
177
- return this;
178
- }
179
130
  /** Runs a command and returns its exit code and output. A string runs under
180
131
  * `bash -c`; an array runs without a shell. With onStdout/onStderr the output
181
132
  * streams as it happens. A timeout is a result (timedOut: true, the output so
@@ -220,6 +171,7 @@ export class Sandbox {
220
171
  ...truncation(dropped),
221
172
  ...(exit?.durationMs === undefined ? {} : { durationMs: exit.durationMs }),
222
173
  ...(processId ? { processId } : {}),
174
+ ...(exit?.outOfMemory ? { outOfMemory: exit.outOfMemory } : {}),
223
175
  };
224
176
  }
225
177
  if (options.check && (result.exitCode !== 0 || result.timedOut))
@@ -245,8 +197,8 @@ export class Sandbox {
245
197
  code: "cancelled",
246
198
  status: 0,
247
199
  hint: stopped
248
- ? `See what it printed: runtime sandbox logs ${id}`
249
- : `Stop it: runtime sandbox kill ${id}, or sandbox.processes.get("${processId}") and kill().`,
200
+ ? `See what it printed: runtime sandboxes logs ${id}`
201
+ : `Stop it: runtime sandboxes kill ${id}, or sandbox.processes.get("${processId}") and kill().`,
250
202
  details: { sandboxId: this.id, processId, stopped },
251
203
  cause: error,
252
204
  });
@@ -257,7 +209,7 @@ export class Sandbox {
257
209
  message: `Lost the output of process ${processId}; the command may still be running.`,
258
210
  code: "connection_error",
259
211
  status: 0,
260
- hint: `Follow it: runtime sandbox logs ${id} -f (sandbox.processes.get("${processId}")), or stop it: runtime sandbox kill ${id}.`,
212
+ hint: `Follow it: runtime sandboxes logs ${id} -f (sandbox.processes.get("${processId}")), or stop it: runtime sandboxes kill ${id}.`,
261
213
  details: { sandboxId: this.id, processId },
262
214
  cause: error,
263
215
  });
@@ -368,275 +320,6 @@ export class Sandbox {
368
320
  throw error;
369
321
  }
370
322
  }
371
- async stop(options = {}) {
372
- this.#keepAlive?.();
373
- return this.#lifecycle("stop", options);
374
- }
375
- async pause(options = {}) {
376
- return this.#lifecycle("pause", options);
377
- }
378
- /** Deletes it for good: stops it if it runs or is paused, deletes its disk
379
- * and paused memory, revokes its previews and ports, and removes it from
380
- * lists. Its snapshots, usage and audit entries stay. Deleting it again
381
- * answers the same. */
382
- async delete(options = {}) {
383
- this.#keepAlive?.();
384
- return deleteSandbox(this.#t, this.id, options);
385
- }
386
- /** Carries on a paused sandbox; for one with a time limit,
387
- * `timeoutSeconds` is its new one. */
388
- async wake(options = {}) {
389
- const { timeoutSeconds, ...rest } = options;
390
- try {
391
- return await this.#lifecycle("wake", rest, timeoutSeconds === undefined ? {} : { timeoutSeconds });
392
- }
393
- catch (error) {
394
- // Waking one that is already awake is done, not a mistake. With a new
395
- // lease asked for, the refusal stands: it was not given.
396
- if (!(error instanceof RuntimeError) || error.code !== "not_paused" || timeoutSeconds)
397
- throw error;
398
- await this.refresh(requestOnly(rest));
399
- if (this.state !== "running" && this.state !== "starting")
400
- throw error;
401
- return this;
402
- }
403
- }
404
- /** More time before its time limit ends (at most an hour ahead of now). A
405
- * sandbox with no time limit needs none: the call answers at once and
406
- * changes nothing. */
407
- async extend(seconds, options = {}) {
408
- return this.#lifecycle("extend", { ...options, wait: false }, { seconds });
409
- }
410
- /** Changes its name, labels, automatic wake, idle pause or persistence;
411
- * fields left out stay as they are. `persistent: true` keeps it running
412
- * while credit lasts (paid only). */
413
- async update(settings, options = {}) {
414
- return this.#lifecycle("update", { ...options, wait: false }, { ...settings });
415
- }
416
- /** Moves it to another image (id, name, name:tag or name@version), keeping
417
- * its id, /workspace (its home: dotfiles, pip --user and npm -g installs),
418
- * volumes, environment, name and previews. Its processes restart, and
419
- * everything else on its old disk (sudo installs, apt packages, /etc) is
420
- * lost; snapshot it first to keep everything. A running sandbox is paused
421
- * first. A switch that fails is undone (`switch_undone`), the sandbox on
422
- * its old image with nothing lost. Charged as a wake. `keep` is optional:
423
- * "workspace" is the only value, and it is sent either way, so an API from
424
- * before it was optional accepts the call too. */
425
- async switchImage(image, options = {}) {
426
- const { keep = "workspace", ...rest } = options;
427
- this.#keep(await this.#t.json({
428
- method: "POST",
429
- path: `/v1/sandboxes/${enc(this.id)}:switch-image`,
430
- body: { image, keep },
431
- // A pause, two boots and the copy between them.
432
- wait: 120,
433
- ...requestOnly(rest),
434
- }));
435
- return this;
436
- }
437
- /** Gives it more or fewer vCPUs or more or less memory by a restart: its id,
438
- * whole disk, volumes, environment, name and previews stay, and its
439
- * programs stop (snapshot it first to keep its memory too). A running sandbox is paused, or stopped if it is
440
- * persistent, and comes back running at the new size on the same server; a
441
- * paused or stopped one is started. Memory bills on the new size from then.
442
- * Refused, with nothing changed, when the server has no room
443
- * (`no_capacity`), above your quota or the trial's 2 vCPU and 4 GiB, or
444
- * while a snapshot or fork of it is being taken. `restart` is optional:
445
- * true is the only value, and it is sent either way, so an API from before
446
- * it was optional accepts the call too. */
447
- async resize(size, options = {}) {
448
- const { restart = true, ...rest } = options;
449
- this.#keep(await this.#t.json({
450
- method: "POST",
451
- path: `/v1/sandboxes/${enc(this.id)}:resize`,
452
- body: { ...size, restart },
453
- // A pause or stop and a cold boot.
454
- wait: 120,
455
- ...requestOnly(rest),
456
- }));
457
- return this;
458
- }
459
- /** Keeps a sandbox with a time limit running past it, in the background,
460
- * until stop() or the returned function ends it: every `everySeconds` (60)
461
- * it extends the limit so that `marginSeconds` (600) remain, never more
462
- * than the hour ahead the API allows. A sandbox with no time limit
463
- * (`endsAt` null) needs none, and is only watched. Running time is billed
464
- * as it is used. A paused sandbox is left paused (a request wakes it,
465
- * unless autoWake is off); a stopped one ends the loop. It does not keep a
466
- * Node or Bun process alive by itself. */
467
- keepAlive(options = {}) {
468
- this.#keepAlive?.();
469
- const every = Math.max(10, options.everySeconds ?? 60) * 1000;
470
- const margin = Math.min(3600, Math.max(60, options.marginSeconds ?? KEEP_ALIVE_MARGIN_SECONDS));
471
- let ended = false;
472
- let timer;
473
- const end = () => {
474
- ended = true;
475
- if (timer)
476
- clearTimeout(timer);
477
- if (this.#keepAlive === end)
478
- this.#keepAlive = undefined;
479
- };
480
- const tick = async () => {
481
- if (ended)
482
- return;
483
- try {
484
- await this.refresh();
485
- if (this.state === "stopped" || this.state === "stopping")
486
- return end();
487
- // No time limit: nothing to extend (an older server sends no endsAt).
488
- if (this.state === "running" && this.info.endsAt !== null) {
489
- const left = (Date.parse(this.info.endsAt ?? this.info.expiresAt) - Date.now()) / 1000;
490
- const need = Math.ceil(margin - left);
491
- if (need >= 1)
492
- await this.extend(Math.min(3600, need));
493
- }
494
- }
495
- catch (error) {
496
- options.onError?.(error);
497
- }
498
- if (ended)
499
- return;
500
- timer = setTimeout(() => void tick(), every);
501
- timer.unref?.();
502
- };
503
- this.#keepAlive = end;
504
- void tick();
505
- return end;
506
- }
507
- /** Days a paused sandbox is kept before deletion (1 to 365). */
508
- async setRetention(days, options = {}) {
509
- return this.#lifecycle("retention", { ...options, wait: false }, { days });
510
- }
511
- /** Keeps this sandbox's whole machine (files, memory, running processes) as
512
- * a snapshot to start new sandboxes from. A running sandbox is paused for
513
- * the moment it takes, then woken; a paused one stays paused. Past
514
- * `timeoutMs` (ten minutes unless given) mid-capture, it stays paused and
515
- * the error names the snapshot. */
516
- async snapshot(options = {}) {
517
- const { idempotencyKey, signal, timeoutMs, ...body } = options;
518
- if (body.mode !== undefined && body.mode !== "memory" && body.mode !== "disk")
519
- throw new TypeError("Snapshot mode must be memory or disk.");
520
- signal?.throwIfAborted();
521
- // One minute, whatever `timeoutMs` said, was too short: pausing a busy
522
- // 16 vCPU, 32 GiB sandbox writes its memory out first (4 October 2026).
523
- const until = performance.now() + (timeoutMs && timeoutMs > 0 ? timeoutMs : SNAPSHOT_DEADLINE_MS);
524
- let running = false;
525
- /** The capture in progress, once the server has one. */
526
- let capturing;
527
- let late = false;
528
- const request = () => {
529
- signal?.throwIfAborted();
530
- const left = Math.ceil(until - performance.now());
531
- if (left <= 0 && capturing) {
532
- late = true;
533
- throw new RuntimeError({
534
- code: "snapshot_timeout",
535
- status: 0,
536
- message: `Snapshot ${capturing} was still capturing at the deadline.${running ? " The sandbox stays paused until it ends: waking it now would fail the capture." : ""}`,
537
- hint: `Wait until runtime.snapshots.get("${capturing}") is ready${running ? ", then wake the sandbox" : ""}, or pass a longer timeoutMs.`,
538
- details: { snapshotId: capturing, sourceSandboxId: this.id },
539
- });
540
- }
541
- if (left <= 0)
542
- throw new RuntimeError({
543
- code: "snapshot_timeout",
544
- status: 0,
545
- message: "Snapshot capture ran past its deadline.",
546
- });
547
- return { ...(signal ? { signal } : {}), timeoutMs: left };
548
- };
549
- await this.refresh(request());
550
- // Straight after a fork or a wake the sandbox is still `resuming`, and
551
- // after a pause still `pausing`: wait for where it is going, or a
552
- // snapshot of it is refused as not paused (user lane, 23 September 2026).
553
- if (this.state === "resuming" || this.state === "starting")
554
- await this.waitFor("running", request());
555
- else if (this.state === "pausing")
556
- await this.waitFor("paused", request());
557
- running = this.state === "running";
558
- // Check before entering cleanup: an abort during refresh must not pause or wake.
559
- const pauseOptions = running ? request() : undefined;
560
- let captured;
561
- let primary;
562
- let failed = false;
563
- try {
564
- if (running)
565
- await this.pause(pauseOptions);
566
- let snapshot = await this.#t.json({
567
- method: "POST",
568
- path: `/v1/sandboxes/${enc(this.id)}:snapshot`,
569
- body,
570
- wait: 10,
571
- ...(idempotencyKey ? { idempotencyKey } : {}),
572
- ...request(),
573
- });
574
- captured = snapshot;
575
- // Prefer: wait is bounded on the server. A capture still in progress
576
- // must keep its source paused, or the worker refuses it after we wake.
577
- while (snapshot.state === "capturing") {
578
- capturing = snapshot.id;
579
- request();
580
- await new Promise((resolve) => setTimeout(resolve, 200));
581
- snapshot = await this.#t.json({
582
- method: "GET",
583
- path: `/v1/snapshots/${enc(snapshot.id)}`,
584
- ...request(),
585
- });
586
- }
587
- if (snapshot.state !== "ready")
588
- throw new RuntimeError({
589
- code: "snapshot_failed",
590
- status: 409,
591
- message: snapshot.error ?? `Snapshot capture ended in state ${snapshot.state}.`,
592
- details: { snapshotId: snapshot.id },
593
- });
594
- if (body.mode === "disk" && snapshot.mode !== "disk")
595
- throw new RuntimeError({
596
- code: "snapshot_mode_mismatch",
597
- status: 409,
598
- message: "The server did not confirm a disk-only snapshot.",
599
- details: { snapshotId: snapshot.id },
600
- });
601
- captured = snapshot;
602
- }
603
- catch (error) {
604
- failed = true;
605
- primary = error;
606
- // A request cut off by the deadline mid-capture is late all the same.
607
- if (capturing && performance.now() >= until)
608
- late = true;
609
- }
610
- // A capture still running needs its source paused: the worker fails one
611
- // whose source woke. The timeout says so and leaves the wake to the caller.
612
- if (running && !late) {
613
- try {
614
- await this.wake();
615
- }
616
- catch (wakeError) {
617
- const recovery = {
618
- ...(captured ? { snapshotId: captured.id } : {}),
619
- sourceSandboxId: this.id,
620
- sourceWakeError: {
621
- ...(wakeError instanceof RuntimeError ? { code: wakeError.code } : {}),
622
- message: wakeError instanceof Error ? wakeError.message : String(wakeError),
623
- },
624
- };
625
- const reported = failed ? primary : wakeError;
626
- if (reported instanceof Error && Object.isExtensible(reported)) {
627
- if (reported instanceof RuntimeError)
628
- Object.assign(reported, { details: { ...reported.details, ...recovery } });
629
- else
630
- Object.assign(reported, recovery);
631
- }
632
- if (!failed)
633
- throw wakeError;
634
- }
635
- }
636
- if (failed)
637
- throw primary;
638
- return captured;
639
- }
640
323
  async fork(options = {}) {
641
324
  const { idempotencyKey, signal, timeoutMs, ...body } = options;
642
325
  const reply = await this.#t.json({
@@ -649,29 +332,6 @@ export class Sandbox {
649
332
  const sandboxes = reply.sandboxes.map((info) => new Sandbox(this.#t, info));
650
333
  return options.count === undefined ? sandboxes[0] : sandboxes;
651
334
  }
652
- /** Starts a stopped persistent sandbox again from its disk (memory is not kept). */
653
- async restart(options = {}) {
654
- return this.#lifecycle("restart", options);
655
- }
656
- async #lifecycle(verb, options, body = {}) {
657
- this.#keep(await this.#t.json({
658
- method: "POST",
659
- path: `/v1/sandboxes/${enc(this.id)}:${verb}`,
660
- body,
661
- wait: options.wait === false ? 0 : 60,
662
- ...requestOnly(options),
663
- }));
664
- return this;
665
- }
666
- async [Symbol.asyncDispose]() {
667
- this.#keepAlive?.();
668
- if (this.#info.state === "stopped")
669
- return;
670
- await this.stop({ wait: false }).catch(() => undefined);
671
- }
672
- toJSON() {
673
- return this.#info;
674
- }
675
335
  }
676
336
  /** Where a reader of a process's output is, in bytes of stdout and stderr
677
337
  * together. Output it did not receive, named by the server or not, becomes a
@@ -747,15 +407,6 @@ function pick(options) {
747
407
  ...(options.timeoutMs ? { timeoutMs: options.timeoutMs + 60_000 } : {}),
748
408
  };
749
409
  }
750
- /** Any other call's request options, as the caller gave them: `timeoutMs` is
751
- * the call's whole deadline, and 0 turns it off. */
752
- function requestOnly(options) {
753
- return {
754
- ...(options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}),
755
- ...(options.signal ? { signal: options.signal } : {}),
756
- ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
757
- };
758
- }
759
410
  /** Sessions: a token your backend makes and hands to your own frontend, which
760
411
  * then runs commands, reads and writes files and reaches previews in this one
761
412
  * sandbox directly, without proxying through your servers. A session cannot
@@ -894,7 +545,7 @@ export class Processes {
894
545
  message: `Lost the output stream of process ${processId} for ${FOLLOW_OUTAGE_MS / 60_000} minutes; the command may still be running.`,
895
546
  code: "connection_error",
896
547
  status: 0,
897
- hint: `Follow it again: runtime sandbox logs ${this.sandboxId} ${processId} -f`,
548
+ hint: `Follow it again: runtime sandboxes logs ${this.sandboxId} ${processId} -f`,
898
549
  details: { sandboxId: this.sandboxId, processId },
899
550
  cause: failure,
900
551
  });
@@ -905,7 +556,7 @@ export class Processes {
905
556
  message: `The output stream of process ${processId} keeps closing before it ends.`,
906
557
  code: "connection_error",
907
558
  status: 0,
908
- hint: `Read what it printed so far: runtime sandbox logs ${this.sandboxId} ${processId}`,
559
+ hint: `Read what it printed so far: runtime sandboxes logs ${this.sandboxId} ${processId}`,
909
560
  details: { sandboxId: this.sandboxId, processId },
910
561
  });
911
562
  }
@@ -997,6 +648,7 @@ export class Process {
997
648
  ...this.info,
998
649
  state: exit.state,
999
650
  exitCode: exit.exitCode,
651
+ ...(exit.outOfMemory ? { outOfMemory: exit.outOfMemory } : {}),
1000
652
  };
1001
653
  return {
1002
654
  exitCode: exit?.exitCode ?? null,
@@ -1004,6 +656,7 @@ export class Process {
1004
656
  timedOut: exit?.timedOut ?? false,
1005
657
  ...truncation(dropped),
1006
658
  processId: this.id,
659
+ ...(exit?.outOfMemory ? { outOfMemory: exit.outOfMemory } : {}),
1007
660
  };
1008
661
  }
1009
662
  /** Sends input. Offsets protect transport retries; signal and timeoutMs cover
@@ -1618,9 +1271,11 @@ export class Files {
1618
1271
  const data = await readFile(localPath);
1619
1272
  const mode = info.mode & 0o777;
1620
1273
  await this.write(remotePath, data, { mode }).catch(async (error) => {
1621
- // A missing parent is made, as a directory upload makes its own.
1274
+ // Make a missing parent, but keep errors about the sandbox or upload.
1622
1275
  const parent = remotePath.slice(0, remotePath.lastIndexOf("/"));
1623
- if (!(error instanceof NotFoundError) || !parent)
1276
+ if (!(error instanceof NotFoundError) ||
1277
+ (error.code !== "file_not_found" && error.code !== "path_not_found") ||
1278
+ !parent)
1624
1279
  throw error;
1625
1280
  await this.mkdir(parent, { parents: true });
1626
1281
  await this.write(remotePath, data, { mode });
@@ -1752,11 +1407,7 @@ export class Files {
1752
1407
  }
1753
1408
  }
1754
1409
  }
1410
+ /** One page of `runtime.sandboxes.list()`, and the pages after it. */
1755
1411
  export function sandboxPage(t, body, query, options = {}) {
1756
- return new Page(body.data.map((info) => new Sandbox(t, info)), body.nextCursor, async (cursor) => sandboxPage(t, await t.json({
1757
- method: "GET",
1758
- path: "/v1/sandboxes",
1759
- query: Object.assign({}, query, { cursor }),
1760
- ...options,
1761
- }), query, options));
1412
+ return machinePage(SANDBOXES, t, (transport, info) => new Sandbox(transport, info), body, query, options);
1762
1413
  }
@@ -0,0 +1,11 @@
1
+ /** `withruntime/sandboxes`: Sandboxes alone. Linux microVMs: create, find and list sandboxes.
2
+ *
3
+ * import { sandboxes } from "withruntime/sandboxes";
4
+ * const client = sandboxes(); // RUNTIME_API_KEY, or this machine's saved connection
5
+ */
6
+ import { Runtime, type RuntimeOptions } from "../client.js";
7
+ export { Sandbox } from "../index.js";
8
+ export { Sandboxes } from "../client.js";
9
+ export type * from "../types.js";
10
+ /** `runtime.sandboxes` of a client made with `options`. */
11
+ export declare function sandboxes(options?: RuntimeOptions): Runtime["sandboxes"];
@@ -0,0 +1,12 @@
1
+ /** `withruntime/sandboxes`: Sandboxes alone. Linux microVMs: create, find and list sandboxes.
2
+ *
3
+ * import { sandboxes } from "withruntime/sandboxes";
4
+ * const client = sandboxes(); // RUNTIME_API_KEY, or this machine's saved connection
5
+ */
6
+ import { Runtime } from "../client.js";
7
+ export { Sandbox } from "../index.js";
8
+ export { Sandboxes } from "../client.js";
9
+ /** `runtime.sandboxes` of a client made with `options`. */
10
+ export function sandboxes(options) {
11
+ return new Runtime(options).sandboxes;
12
+ }
@@ -0,0 +1,9 @@
1
+ /** `withruntime/snapshots`: Snapshots alone. Saved sandboxes, disk and memory, to start or fork from.
2
+ *
3
+ * import { snapshots } from "withruntime/snapshots";
4
+ * const client = snapshots(); // RUNTIME_API_KEY, or this machine's saved connection
5
+ */
6
+ import { Runtime, type RuntimeOptions } from "../client.js";
7
+ export { Snapshots, type Snapshot, type SnapshotOptions } from "../snapshots.js";
8
+ /** `runtime.snapshots` of a client made with `options`. */
9
+ export declare function snapshots(options?: RuntimeOptions): Runtime["snapshots"];
@@ -0,0 +1,11 @@
1
+ /** `withruntime/snapshots`: Snapshots alone. Saved sandboxes, disk and memory, to start or fork from.
2
+ *
3
+ * import { snapshots } from "withruntime/snapshots";
4
+ * const client = snapshots(); // RUNTIME_API_KEY, or this machine's saved connection
5
+ */
6
+ import { Runtime } from "../client.js";
7
+ export { Snapshots } from "../snapshots.js";
8
+ /** `runtime.snapshots` of a client made with `options`. */
9
+ export function snapshots(options) {
10
+ return new Runtime(options).snapshots;
11
+ }
@@ -17,7 +17,8 @@ export type Snapshot = {
17
17
  diskMiB: number;
18
18
  memoryGuarantee: string;
19
19
  };
20
- retentionDays: number;
20
+ /** Null: kept while your account has credit, up to a year from when it was taken. */
21
+ retentionDays: number | null;
21
22
  /** True while raw fork memory awaits verified compression. The snapshot
22
23
  * remains ready for starts on its current host, with no storage charge or
23
24
  * off-host copy until final publication. Absent on older servers. */
@@ -39,7 +40,8 @@ export type Snapshot = {
39
40
  error: string | null;
40
41
  createdAt: string;
41
42
  readyAt: string | null;
42
- expiresAt: string;
43
+ /** Null while it is kept as long as your account has credit. */
44
+ expiresAt: string | null;
43
45
  [key: string]: unknown;
44
46
  };
45
47
  export type SnapshotOptions = {
@@ -47,7 +49,8 @@ export type SnapshotOptions = {
47
49
  mode?: "memory" | "disk";
48
50
  name?: string;
49
51
  labels?: Record<string, string>;
50
- retentionDays?: number;
52
+ /** Days, or null to keep it while your account has credit. */
53
+ retentionDays?: number | null;
51
54
  };
52
55
  /** `runtime.snapshots`. Take one with `sbx.snapshot()`, start from one with
53
56
  * `runtime.sandboxes.create({ snapshot: id })`, or do both with `sbx.fork()`. */
@@ -71,5 +74,5 @@ export declare class Snapshots {
71
74
  ifLabels?: Record<string, string>;
72
75
  }, options?: RequestOptions): Promise<Snapshot>;
73
76
  delete(id: string, options?: RequestOptions): Promise<void>;
74
- extend(id: string, retentionDays: number, options?: RequestOptions): Promise<Snapshot>;
77
+ extend(id: string, retentionDays: number | null, options?: RequestOptions): Promise<Snapshot>;
75
78
  }