@prized/sdk 0.1.1 → 0.1.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
@@ -13,6 +13,10 @@ console.log(stdout); // 42
13
13
  await sandbox.delete();
14
14
  ```
15
15
 
16
+ Save it as `hello.mjs` and run `node hello.mjs` (Node 18 or later) with
17
+ `PRIZED_API_KEY` set ([Authentication](#authentication)). Top-level `await`
18
+ needs an ES module, so a `.js` file needs `"type": "module"` in package.json.
19
+
16
20
  Sandboxes are in beta. A workspace on free credit alone runs them on the
17
21
  free tier: up to 10 at once, each up to 2 vCPU and 4 GB of memory and 60
18
22
  minutes of running. A top-up or a paid plan lifts the limits: a sandbox then
@@ -60,6 +64,8 @@ to fewer permissions needs **Sandboxes: Write** to create, run commands and
60
64
  move files; **Sandboxes: Read** is enough for `list`, `connect`, `refresh` and
61
65
  `templates`.
62
66
 
67
+ Your coding agent can install the Prized skill with `npx skills add prized-dev/skills -g -y` (it should ask you first), so it knows when and how to use Prized the next time you ask for a sandbox.
68
+
63
69
  Every entry point (`create`, `connect`, `list`, `deleteByLabels`, `templates`) takes the same
64
70
  client options:
65
71
 
@@ -72,7 +78,7 @@ client options:
72
78
  | `maxRetries` | `3` | See [Retries](#retries-and-timeouts). |
73
79
  | `requestTimeoutMs` | `60000` | Per API request. |
74
80
  | `pollIntervalMs` | `500` | The longest the SDK's own polling goes between checks of the sandbox (the first come sooner). It does not cover the request itself, or the time the API holds a create, pause or resume for you (up to 8 seconds). |
75
- | `pollOutageMs` | `120000` | How long a wait keeps polling through a dropped connection (or a 5xx, or a rate limit) before it gives up: a streamed or long `exec` then stops the command, a handle's `wait()` throws. `exec` and `wait()` take it per call too. |
81
+ | `pollOutageMs` | `120000` | How long a wait keeps polling through a dropped connection (or a 5xx, or a rate limit) before it gives up: a streamed or long `exec` then stops the command, a handle's `wait()` throws. A background, streamed or long `exec` keeps sending its start for as long. `exec` and `wait()` take it per call too. |
76
82
 
77
83
  Your API key goes with every request, so a plain `http` URL to a host other
78
84
  than this machine is refused with `invalid_argument` before anything is sent,
@@ -181,8 +187,10 @@ await Promise.all(mine.map((s) => s.delete()));
181
187
  ### `Sandbox.deleteByLabels(labels, options?)`
182
188
 
183
189
  Deletes every sandbox carrying all of `labels` (at least one) in one call and
184
- resolves with their ids. It is safe to repeat (one already deleting is listed
185
- again; none matching resolves `[]`). A sandbox being deleted still counts
190
+ resolves with their ids. Each request selects the current matches, including
191
+ sandboxes created since an earlier request. If an answer is lost, the SDK
192
+ throws without repeating the delete; list the matches before deleting again.
193
+ A sandbox being deleted still counts
186
194
  toward your quota until `Sandbox.list({ labels })` no longer returns it.
187
195
 
188
196
  ```ts
@@ -287,7 +295,9 @@ off by an idle connection and its output is never truncated. The result is
287
295
  the same either way. If the connection drops meanwhile, the SDK keeps
288
296
  polling for up to 2 minutes (`pollOutageMs`, on the call or the client)
289
297
  before it gives up and stops the command; a refused key, or an answer that
290
- settles it (the sandbox deleted or not running), stops it at once.
298
+ settles it (the sandbox deleted or not running), stops it at once. Its
299
+ start rides one out the same way: it is sent again for up to `pollOutageMs`,
300
+ and a resend finds the first start, so the command runs once.
291
301
  Background and streamed commands run with `PYTHONUNBUFFERED=1` unless
292
302
  their environment already sets it (`env: { PYTHONUNBUFFERED: '' }` keeps
293
303
  Python's buffering), so a Python program's output arrives as it prints.
@@ -295,13 +305,17 @@ Python's buffering), so a Python program's output arrives as it prints.
295
305
  Commands run as the user `prized` (home `/home/prized`, no sudo). The
296
306
  templates come with git, curl, Python 3 with pip and venv, Node 22 with npm
297
307
  and pnpm; `prized-agent-v1` adds Claude Code and Codex. Install what else
298
- you need into your home: Python packages into a venv
299
- (`python3 -m venv ~/.venv && ~/.venv/bin/pip install numpy`; plain
300
- `pip install`, `--user` too, is refused), and `npm` with a user prefix.
308
+ you need into your home: `pip install numpy` goes to `~/.local` (or use a
309
+ venv: `python3 -m venv ~/.venv && ~/.venv/bin/pip install numpy`), and
310
+ `npm install -g` to `~/.local` too.
301
311
 
302
312
  ## Background commands
303
313
 
304
- `background: true` starts the command and returns a `CommandHandle` at once.
314
+ `background: true` starts the command and returns a `CommandHandle` once it
315
+ has started. A start that meets a dropped connection or a 5xx is sent again
316
+ for up to `pollOutageMs` once its own retries run out (a resend finds the
317
+ first start, so the command runs once), so the call can take that long while
318
+ the connection is down.
305
319
  The command runs in its own session: it keeps running after your process
306
320
  exits, survives a pause and resume, and while it runs, a kill reaches every
307
321
  process it started (not one that left its process group, such as a daemon).
@@ -413,11 +427,6 @@ prized sandbox timeout sbx_0123456789abcdef --in 30m # deleted 30 minutes from
413
427
  On a paid plan an extend stops at 7 days from now; on the free tier neither
414
428
  moves the end later.
415
429
 
416
- <!-- Held while sandbox ports at a URL are switched off: this section stays
417
- out of the rendered README until they are on. In the change that turns them
418
- on, delete these five lines and the closing line after the section, take
419
- "a sandbox's ports at a URL" out of "Not yet" under Limits, and delete the
420
- test "the README holds A port at a URL" (test/preview.test.ts).
421
430
  ## A port at a URL
422
431
 
423
432
  `sandbox.previewUrl(port)` is the https URL a port of the sandbox answers at:
@@ -444,15 +453,47 @@ the preview URL, and one that points anywhere else comes back as
444
453
  `Prized-Location` instead, which no client follows. To follow a redirect,
445
454
  resolve its `Location` against the preview URL and send `previewHeaders()`
446
455
  again only when the result has the preview URL's origin, scheme included.
447
- Call preview URLs from server-side code: a browser hides `Location` from
448
- `'manual'`.
456
+ Call preview URLs with the key from server-side code: a browser hides
457
+ `Location` from `'manual'`.
449
458
 
450
459
  Nothing is created: the URL is the sandbox's own, and every port has one
451
- (but 22 and 81). A request to a paused sandbox resumes it and waits up to 10
452
- seconds for it, as `exec` does; send `Prized-Auto-Resume: false` to get
453
- `503` instead. `previewUrl` throws `preview_unavailable` where this is not
454
- turned on.
455
- -->
460
+ (but 22 and 81). A request with the key to a paused sandbox resumes it and
461
+ waits up to 10 seconds for it, as `exec` does; send
462
+ `Prized-Auto-Resume: false` to get `503` instead. `previewUrl` throws
463
+ `preview_unavailable` where this is not turned on.
464
+
465
+ A person in a browser, or a client that cannot send the key in a header (a
466
+ WebSocket from a browser page, a program you hand one port to and not your
467
+ key), takes a short-lived token in the URL instead:
468
+
469
+ ```ts
470
+ const { browserUrl, url, expiresAt } = await sandbox.previewToken(3000, { expiresIn: 600 });
471
+ // browserUrl: https://3000-sbx-….prized.link/?prized_token=… opens in a tab or an iframe
472
+ const socket = new WebSocket(url.replace('https:', 'wss:'));
473
+ ```
474
+
475
+ `browserUrl` is the port on `prized.link`, a domain of its own for pages your
476
+ sandboxes serve: open it in a tab or embed it in an iframe. The browser swaps
477
+ the token for a cookie for that one host, so the page's own assets, fetches
478
+ and WebSockets work, and a tab's first visit shows a short notice that the
479
+ page is hosted by a Prized user. `url` is the same token on the preview URL,
480
+ for programs. The token opens that one port until `expiresAt` (an hour by
481
+ default, a day at most), or until your key is revoked; the app never sees
482
+ it. To let anyone reach a port with no credential at all, make it public:
483
+
484
+ ```ts
485
+ const { browserUrl } = await sandbox.setPortPublic(3000, true, { expiresIn: 7 * 86400 });
486
+ await sandbox.setPortPublic(3000, false); // private again
487
+ ```
488
+
489
+ Tokens and public ports need a paid workspace (`share_links_need_payment`
490
+ otherwise), and making a port public takes the workspace owner's key. On the
491
+ preview URL they answer programs, `fetch` and WebSockets but not a page
492
+ opened in a browser tab (`403 ingress_browser_page`, which names the
493
+ `browserUrl`); on `browserUrl` they open in a browser too. Neither wakes a
494
+ paused sandbox: a request gets `503` with `error: 'sandbox_not_running'`
495
+ until something with the key resumes it. Making a port public or private
496
+ again ends the tokens made for it.
456
497
 
457
498
  ## Fork
458
499
 
@@ -528,7 +569,7 @@ try {
528
569
  | `sandbox_not_running` | 409 | The sandbox is not running and the call could not wake it: it is resizing (`refresh()` until it leaves `resizing`, then retry), failed, or being deleted. A paused sandbox is resumed by any command or file call. The SDK never turns waking off; a request that carries `Prized-Auto-Resume: false` (added by a custom `fetch`, say) gets this for a paused or starting sandbox too: drop the header to let the call wake it and wait. |
529
570
  | `sandbox_not_ready` | 503 | A paused or starting sandbox did not reach running within the edge's 10-second wait. The wait starts when the edge, having checked the key, finds the sandbox not running, and calls with the same key arriving during it share it, so a later call may wait less. Nothing ran; retry. |
530
571
  | `parent_not_running` | 409 | Only a running or paused sandbox can be forked. |
531
- | `parent_not_forkable` | 409 | This sandbox cannot be forked. `reason` `resized_while_paused`: it has no memory to copy; resume it, then fork it. `reason` `parent_too_big`: its machine cannot hold even one copy beside it; fork a smaller sandbox. |
572
+ | `parent_not_forkable` | 409 | This sandbox cannot be forked. `reason` `resized_while_paused` or `memory_discarded`: it has no memory to copy; resume it, then fork it. `reason` `parent_too_big`: its machine cannot hold even one copy beside it; fork a smaller sandbox. |
532
573
  | `fork_capacity` | 409 | The parent's machine has room for only `fits` more copies (`err.body?.fits`). If `fits` is above 0, fork that many or fewer; or try again later, or start a new sandbox and fork that one, keeping the parent until it runs. The new sandbox and each copy count toward your quota: at the limit, delete sandboxes you no longer need with `delete({ wait: true })` (a deleting sandbox still counts), or top up on the free tier. Copy out what you need first, and never delete a parent while a copy forked from it is still starting. |
533
574
  | `fork_unsupported_host` | 409 | The parent's machine cannot start copies of it. With `reason` `host_retiring` (`err.reason`), the machine is being replaced: it keeps the parent but starts no new sandboxes, so no retry or smaller `count` helps. Create a new sandbox, set it up again, and fork that one. |
534
575
  | `sandbox_capacity` | 503 | No sandbox capacity is free right now. Nothing was created or billed; the SDK retries it after `Retry-After`. |
@@ -565,18 +606,21 @@ try {
565
606
  `resume`, `fork` and `delete` (the request, its retries and the wait, its
566
607
  polls included; with `wait: false` too), twice `requestTimeoutMs` (2
567
608
  minutes by default) for `connect`, `list`, `templates`, `refresh` and
568
- `keepAlive`, and 10 minutes for a file transfer. `Retry-After` is honoured
609
+ `keepAlive`, and 10 minutes for a file transfer, whose attempt is given up
610
+ after 2 minutes with no bytes moving (a dropped connection) and sent again
611
+ by the rules below (on Node's own `fetch`; elsewhere, or with a `fetch` you
612
+ pass, an upload is watched only once its answer starts). `Retry-After` is honoured
569
613
  up to a minute. A retry that could not start in time is not sent: the call
570
614
  throws `timeout`. A wait's last poll goes out just before its deadline.
571
- - A command, a `files.remove` or a `kill` is sent again only when it
615
+ - A command, a `files.remove`, a `kill` or `deleteByLabels` is sent again only when it
572
616
  provably never arrived: the connection never opened, or the answer says so
573
617
  (rate limited, the sandbox not ready). If an answer is lost after it was
574
618
  delivered, the SDK throws (`sandbox_unreachable`, `network_error`,
575
- `timeout`) rather than risk running a command twice.
619
+ `timeout`) rather than risk repeating the operation on work that changed.
576
620
  - A file write (`files.write`, `files.upload`) goes up under a temporary name
577
621
  beside the file, which is sent again like a read (a lost answer, a 502, 503
578
- or 504), every copy to the same name and none after the transfer's 10
579
- minutes, so one that timed out is not sent again. One command that is never
622
+ or 504, 2 minutes with no bytes moving), every copy to the same name and
623
+ none after the transfer's 10 minutes. One command that is never
580
624
  sent again then puts it in place: if that command's answer is lost, the SDK
581
625
  throws rather than risk overwriting what the sandbox wrote to the file since.
582
626
  - Did it run? When a command answered in one call (`exec` without
@@ -594,7 +638,8 @@ try {
594
638
  says the same of its signal
595
639
  (`status()` tells you whether the command is still running). A background
596
640
  command (or a streamed or long `exec`, which runs as one) whose start got
597
- no clear answer is stopped by the SDK, and the error says so, or, when the
641
+ no clear answer for longer than `pollOutageMs` is stopped by the SDK, and
642
+ the error says so, or, when the
598
643
  SDK could not confirm the stop, that it may still be running and under
599
644
  which run id. So is a streamed or long `exec` whose polling fails while
600
645
  it runs (a dropped connection or a 5xx for longer than `pollOutageMs`, a
@@ -652,8 +697,8 @@ runtime) are on the pricing and docs pages at prized.dev; a create or resume
652
697
  past one fails with `sandbox_quota_exceeded`.
653
698
 
654
699
  Not yet: custom templates (from a Dockerfile), egress allow and deny lists,
655
- a sandbox's ports at a URL, more than 32 vCPU or 128 GB, GPUs, and regions
656
- other than US West.
700
+ a port URL that opens without an API key (for a browser or a webhook), more
701
+ than 32 vCPU or 128 GB, GPUs, and regions other than US West.
657
702
 
658
703
  ## Runtimes
659
704
 
@@ -104,7 +104,9 @@ export interface ExecOptions {
104
104
  * its polls fail (no answer, a 5xx, a rate limit), in ms, before it gives
105
105
  * up and stops the command. Default: the client's `pollOutageMs`
106
106
  * (120 000). A refused key, or an answer that settles the run (the
107
- * sandbox deleted or not running, the run gone), stops it at once.
107
+ * sandbox deleted or not running, the run gone), stops it at once. The
108
+ * start of a background, streamed or long command is sent again for as
109
+ * long (a resend finds the first start, so the command runs once).
108
110
  */
109
111
  pollOutageMs?: number;
110
112
  }
@@ -477,7 +477,48 @@ async function cleanupOnceGone(toolbox, id) {
477
477
  }
478
478
  }
479
479
  }
480
- async function startBackground(toolbox, command, cwd, env, timeoutMs, signal, unbuffered) {
480
+ /**
481
+ * Send a start script. It is keyed by a run id the SDK chose, so a resend
482
+ * finds the first start instead of starting the command twice. Once one
483
+ * copy's retries run out, it is sent again through an outage of up to
484
+ * `outageMs` (pollOutageMs), paced as a wait's polls are (Outage): an edge
485
+ * that restarts mid-start does not cost the command, as it does not cost a
486
+ * running one its wait. Each copy's error counts the copies before it
487
+ * (`acted`), so one that gives up says the command may have started when any
488
+ * copy may have reached the sandbox. The Python SDK's _send_start.
489
+ */
490
+ async function sendStart(toolbox, req, signal, outageMs) {
491
+ const config = toolbox.config;
492
+ const outage = new Outage(outageMs);
493
+ // Whether an earlier copy may have reached the sandbox: an abort between
494
+ // copies then says the command may have started, else that it never did.
495
+ let acted = false;
496
+ const aborted = (cause) => (0, outcome_js_1.withAbort)(new errors_js_1.PrizedError('aborted', null, { sandboxId: toolbox.sandboxId, cause }), (0, outcome_js_1.startEffect)(), acted ? 'maybe' : 'never');
497
+ for (;;) {
498
+ const sentAt = config.now();
499
+ try {
500
+ return await (0, toolbox_js_1.rawExec)(toolbox, req, { retry: 'safe', signal, deadline: outage.deadline(sentAt), effect: (0, outcome_js_1.startEffect)(), acted });
501
+ }
502
+ catch (error) {
503
+ if (!(error instanceof errors_js_1.PrizedError))
504
+ throw error;
505
+ acted = acted || (0, outcome_js_1.mayHaveActed)(error);
506
+ if (signal?.aborted)
507
+ throw error;
508
+ const step = outage.failed(error, sentAt, config.now(), (0, http_js_1.cutByDeadline)(error), (tries) => (0, http_js_1.backoffMs)(config, tries));
509
+ // What a copy met, not the window's cut.
510
+ if (step.pause === null)
511
+ throw step.failure === error ? error : (0, outcome_js_1.startGaveUp)(step.failure, acted);
512
+ try {
513
+ await config.sleep(step.pause, signal);
514
+ }
515
+ catch (abort) {
516
+ throw abort instanceof errors_js_1.PrizedError && abort.code === 'aborted' ? aborted(step.failure) : abort;
517
+ }
518
+ }
519
+ }
520
+ }
521
+ async function startBackground(toolbox, command, cwd, env, timeoutMs, signal, unbuffered, outageMs) {
481
522
  // Nothing sent yet: nothing to stop.
482
523
  if (signal?.aborted)
483
524
  throw (0, outcome_js_1.withOutcome)(new errors_js_1.PrizedError('aborted', null, { sandboxId: toolbox.sandboxId }), (0, outcome_js_1.startEffect)(), 'never');
@@ -489,10 +530,7 @@ async function startBackground(toolbox, command, cwd, env, timeoutMs, signal, un
489
530
  // Launched, but no pid came within the start's own wait: it may yet run.
490
531
  let unsettled = false;
491
532
  try {
492
- const result = await (0, toolbox_js_1.rawExec)(toolbox, { command: (0, shell_js_1.startScript)(id, command, timeoutMs === null ? null : timeoutMs / 1000, unbuffered), cwd, env, timeoutMs: 60_000 },
493
- // Keyed by a run id this call chose, so a resend finds the first start
494
- // instead of starting the command twice.
495
- { retry: 'safe', signal, effect: (0, outcome_js_1.startEffect)() });
533
+ const result = await sendStart(toolbox, { command: (0, shell_js_1.startScript)(id, command, timeoutMs === null ? null : timeoutMs / 1000, unbuffered), cwd, env, timeoutMs: 60_000 }, signal, outageMs);
496
534
  pid = Number(result.stdout.trim());
497
535
  if (result.exitCode !== 0 || !Number.isInteger(pid) || pid <= 0) {
498
536
  unsettled = result.exitCode === shell_js_1.NO_PID_EXIT;
@@ -538,7 +576,7 @@ async function execCommand(toolbox, command, options = {}) {
538
576
  const cwd = options.cwd === undefined ? undefined : (0, shell_js_1.sandboxPath)(options.cwd);
539
577
  if (options.background) {
540
578
  const limit = options.timeoutMs ? options.timeoutMs : null;
541
- const handle = await startBackground(toolbox, text, cwd, options.env, limit, options.signal, true);
579
+ const handle = await startBackground(toolbox, text, cwd, options.env, limit, options.signal, true, outageMs);
542
580
  handle.timeoutMs = limit;
543
581
  return handle;
544
582
  }
@@ -546,7 +584,7 @@ async function execCommand(toolbox, command, options = {}) {
546
584
  const streamed = options.onStdout !== undefined || options.onStderr !== undefined;
547
585
  if (streamed || timeoutMs === 0 || timeoutMs > exports.MAX_BLOCKING_EXEC_MS) {
548
586
  const limit = timeoutMs === 0 ? null : timeoutMs;
549
- const handle = await startBackground(toolbox, text, cwd, options.env, limit, options.signal, streamed);
587
+ const handle = await startBackground(toolbox, text, cwd, options.env, limit, options.signal, streamed, outageMs);
550
588
  const waitOptions = {};
551
589
  if (options.onStdout)
552
590
  waitOptions.onStdout = options.onStdout;
@@ -13,9 +13,10 @@ export interface WriteOptions {
13
13
  }
14
14
  /**
15
15
  * The sandbox's file transfers take at most this long, retries included:
16
- * the edge's own limit on one transfer. A write's upload sent again (a
17
- * staged one is, after a lost answer or a 502, 503 or 504) is sent only
18
- * while time is left, so a transfer that timed out is not.
16
+ * the edge's own limit on one transfer. An attempt that moves no bytes for
17
+ * timing.transferStallMs (2 minutes) is given up; a download, or a write's
18
+ * staged upload, is then sent again (as after a lost answer or a 502, 503 or
19
+ * 504) while time is left.
19
20
  */
20
21
  export declare const FILE_TIMEOUT_MS: number;
21
22
  /** The publish script's own exits (the Python SDK's too). */
@@ -60,8 +61,8 @@ export declare class Files {
60
61
  * stays executable; a new file gets the sandbox's default (0600 in
61
62
  * prized-base-v1 and prized-agent-v1). `mode` sets the permission bits either way.
62
63
  * The bytes go up under a temporary name beside the file, sent again if
63
- * need be (a lost answer, a 502, 503 or 504; all copies within the
64
- * transfer's 10 minutes),
64
+ * need be (a lost answer, a 502, 503 or 504, 2 minutes with no bytes
65
+ * moving; all copies within the transfer's 10 minutes),
65
66
  * and one command that is never sent again puts them in place: a write
66
67
  * whose answer to that command was lost throws, since it may have landed
67
68
  * and the sandbox written the file since (read it back, or write again,
package/dist/cjs/files.js CHANGED
@@ -11,9 +11,10 @@ const shell_js_1 = require("./shell.js");
11
11
  const toolbox_js_1 = require("./toolbox.js");
12
12
  /**
13
13
  * The sandbox's file transfers take at most this long, retries included:
14
- * the edge's own limit on one transfer. A write's upload sent again (a
15
- * staged one is, after a lost answer or a 502, 503 or 504) is sent only
16
- * while time is left, so a transfer that timed out is not.
14
+ * the edge's own limit on one transfer. An attempt that moves no bytes for
15
+ * timing.transferStallMs (2 minutes) is given up; a download, or a write's
16
+ * staged upload, is then sent again (as after a lost answer or a 502, 503 or
17
+ * 504) while time is left.
17
18
  */
18
19
  exports.FILE_TIMEOUT_MS = 10 * 60_000;
19
20
  function byteSize(data) {
@@ -168,6 +169,7 @@ class Files {
168
169
  retry: 'safe',
169
170
  read: 'bytes',
170
171
  timeoutMs: exports.FILE_TIMEOUT_MS,
172
+ stallMs: http_js_1.timing.transferStallMs,
171
173
  deadline: this.toolbox.config.now() + exports.FILE_TIMEOUT_MS,
172
174
  sandboxId: this.toolbox.sandboxId,
173
175
  });
@@ -180,8 +182,8 @@ class Files {
180
182
  * stays executable; a new file gets the sandbox's default (0600 in
181
183
  * prized-base-v1 and prized-agent-v1). `mode` sets the permission bits either way.
182
184
  * The bytes go up under a temporary name beside the file, sent again if
183
- * need be (a lost answer, a 502, 503 or 504; all copies within the
184
- * transfer's 10 minutes),
185
+ * need be (a lost answer, a 502, 503 or 504, 2 minutes with no bytes
186
+ * moving; all copies within the transfer's 10 minutes),
185
187
  * and one command that is never sent again puts them in place: a write
186
188
  * whose answer to that command was lost throws, since it may have landed
187
189
  * and the sandbox written the file since (read it back, or write again,
@@ -261,6 +263,7 @@ class Files {
261
263
  retry,
262
264
  effect,
263
265
  timeoutMs: exports.FILE_TIMEOUT_MS,
266
+ stallMs: http_js_1.timing.transferStallMs,
264
267
  deadline: this.toolbox.config.now() + exports.FILE_TIMEOUT_MS,
265
268
  sandboxId: this.toolbox.sandboxId,
266
269
  });
@@ -32,7 +32,9 @@ export interface ClientOptions {
32
32
  * rate limit), in ms, before it gives up: a streamed or long `exec` then
33
33
  * stops the command, a handle's `wait()` throws. Default 120 000. `0`
34
34
  * gives up once one poll's retries run out. `exec` and `wait()` take it
35
- * per call too.
35
+ * per call too. The start of a background, streamed or long `exec` is
36
+ * sent again for as long, so an edge that restarts mid-start does not
37
+ * cost the command.
36
38
  */
37
39
  pollOutageMs?: number;
38
40
  /** Sleep seam (tests inject one that never really waits). */
@@ -54,13 +56,21 @@ export interface Config {
54
56
  sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
55
57
  now: () => number;
56
58
  random: () => number;
59
+ /**
60
+ * Whether an upload's body goes out as a stream the SDK feeds piece by
61
+ * piece, so a stalled one is seen (RequestSpec.stallMs): Node's own fetch
62
+ * only, which asks for each piece once the last has gone into the socket.
63
+ * Elsewhere (another runtime, a fetch you pass) the body goes as it is.
64
+ */
65
+ streamBodies: boolean;
57
66
  }
58
67
  export declare function abortableSleep(ms: number, signal?: AbortSignal): Promise<void>;
59
68
  /**
60
69
  * How long a wait (a streamed or long exec, a handle's wait()) keeps polling
61
70
  * while its polls fail before it gives up (pollOutageMs), and a streamed or
62
- * long exec stops its command: long enough to ride out a dropped connection
63
- * or an edge that restarts. The Python SDK's DEFAULT_POLL_OUTAGE.
71
+ * long exec stops its command, and how long a start is sent again: long
72
+ * enough to ride out a dropped connection or an edge that restarts (about
73
+ * 70 s, the last time it crashed). The Python SDK's DEFAULT_POLL_OUTAGE.
64
74
  */
65
75
  export declare const DEFAULT_POLL_OUTAGE_MS = 120000;
66
76
  /** A pollOutageMs given (a number of ms ≥ 0), or undefined. */
@@ -97,8 +107,8 @@ export declare function warmEdge(config: Config): Promise<void>;
97
107
  * Retries network failures, 429/502/503/504, and everything `exec`
98
108
  * retries (the edge's 500 control_plane_unavailable among them). A GET
99
109
  * retries any 500 too (the API's `internal`).
100
- * - `exec`: a command, an upload or a remove: what the sandbox does
101
- * meanwhile could be undone by a second copy. Retried only when it
110
+ * - `exec`: a command, an upload, a remove or a label-selected delete:
111
+ * a second copy could affect later work. Retried only when it
102
112
  * provably never arrived: a connection that never opened, or an answer
103
113
  * saying so (the edge's rate limit, its lookup failing). A lost answer
104
114
  * is never retried: it may already have run.
@@ -120,7 +130,7 @@ export type RetryMode = 'safe' | 'exec' | 'staged';
120
130
  export declare function neverSent(error: unknown, depth?: number): boolean;
121
131
  export declare function retryable(error: PrizedError, mode: RetryMode, method: RequestSpec['method']): boolean;
122
132
  export interface RequestSpec {
123
- method: 'GET' | 'POST' | 'DELETE';
133
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
124
134
  /** The URL, or one built per attempt from the ms left before `deadline`
125
135
  * (Infinity without one): a held request asks only for what is left. */
126
136
  url: string | ((leftMs: number) => string);
@@ -139,6 +149,14 @@ export interface RequestSpec {
139
149
  sandboxId?: string | null;
140
150
  /** How to read a 2xx body. Default `json`. */
141
151
  read?: 'json' | 'bytes' | 'none';
152
+ /**
153
+ * A file transfer's no-progress bound: an attempt that moves no bytes
154
+ * either way for this long (a connection dropped without a reset) ends as
155
+ * a timeout, sent again by `retry`'s rules while `deadline` allows. An
156
+ * upload's sending is watched only where its body is streamed
157
+ * (Config.streamBodies); its answer always is.
158
+ */
159
+ stallMs?: number;
142
160
  /**
143
161
  * Told of every attempt that failed (no answer, an error answer, or a 2xx
144
162
  * that could not be read), before it is retried or thrown. A create or
@@ -153,6 +171,12 @@ export interface RequestSpec {
153
171
  * its error says so too.
154
172
  */
155
173
  effect?: Effect;
174
+ /**
175
+ * An earlier copy of this keyed request may have reached the sandbox (the
176
+ * background start, sent again through an outage), so its error says so
177
+ * too.
178
+ */
179
+ acted?: boolean;
156
180
  }
157
181
  export interface HttpResult {
158
182
  status: number;
@@ -215,6 +239,25 @@ export declare class Polls {
215
239
  }
216
240
  /** Delay before retry number `attempt` (0-based): exponential with jitter. */
217
241
  export declare function backoffMs(config: Config, attempt: number): number;
242
+ /**
243
+ * How long a file transfer may move no bytes before the attempt is given up
244
+ * (sdk/retry-spec.json transferStallMs; the Python SDK's TRANSFER_STALL).
245
+ * The edge holds a request about 20 s at most before it reads the body (its
246
+ * admission queue, then waking a paused sandbox), and ends a body that brings
247
+ * nothing for 60 s itself: kept above that, a stalled copy is over at the
248
+ * edge before the next one goes to the same staged name, so the two never
249
+ * race. Once an upload's last byte is handed over, its answer is waited for
250
+ * that and a second more for every uploadAnswerBytesPerSecond it sent: what
251
+ * the socket's buffers, or a proxy that takes a whole body before it sends it
252
+ * on, still hold moves unseen, and must not read as a stall (the Python SDK's
253
+ * UPLOAD_ANSWER_RATE). Mutable for tests only.
254
+ */
255
+ export declare const timing: {
256
+ transferStallMs: number;
257
+ uploadAnswerBytesPerSecond: number;
258
+ };
259
+ /** A streamed upload goes out this many bytes at a time. */
260
+ export declare const UPLOAD_CHUNK: number;
218
261
  /**
219
262
  * What an error answer (not a 2xx: request() judges those) says about
220
263
  * whether the sandbox acted: `never` for the codes that prove the request